diff --git a/projects/liza22/pom.xml b/projects/liza22/pom.xml new file mode 100644 index 0000000..cec6fb5 --- /dev/null +++ b/projects/liza22/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + ru.mipt.diht.students + parent + 1.0-SNAPSHOT + + ru.mipt.diht.students + liza22 + 1.0-SNAPSHOT + liza22 + http://maven.apache.org + + UTF-8 + + + + org.apache.commons + commons-collections4 + 4.0 + + + + junit + + junit + + 4.4 + + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.8 + 1.8 + + + + + diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/App.java b/projects/liza22/src/main/java/ru/mipt/diht/students/App.java new file mode 100644 index 0000000..31d6fa9 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/App.java @@ -0,0 +1,11 @@ +package ru.mipt.diht.students; + +/** + * Hello world! + * + */ +public class App { + public static void main(String[] args) { + System.out.println("Hello World!"); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/.gitignore b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/.gitignore new file mode 100644 index 0000000..031401d --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/.gitignore @@ -0,0 +1 @@ +twitter.cfg \ No newline at end of file diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/pom.xml b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/pom.xml new file mode 100644 index 0000000..d3f7776 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + com.twitter.stream + twitterstream + 1.0 + + + 1.7 + 1.7 + + 1.48 + 4.0.4 + + + + + com.beust + jcommander + ${jcommander-version} + + + org.twitter4j + twitter4j-core + ${twitter4j-version} + + + org.twitter4j + twitter4j-stream + ${twitter4j-version} + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + TwitterStream + lib/*d + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.5.1 + + + copy-dependencies + package + + copy-dependencies + + + runtime + ${project.build.directory}/lib/ + + + + + + + + + \ No newline at end of file diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/TwitterStream.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/TwitterStream.java new file mode 100644 index 0000000..01aaba5 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/TwitterStream.java @@ -0,0 +1,85 @@ +import com.beust.jcommander.JCommander; +import config.Arguments; +import config.Constants; +import config.TwitterConfig; +import core.handling.TweetHandler; +import core.handling.TweetHandlerFactory; +import core.providing.TweetsProvider; +import core.providing.TweetsProviderFactory; +import model.Mode; + +import java.io.*; + +/** + * Main Class. + */ + +public class TwitterStream { + + public static final int LINE_LENGTH = 1024; + + public static void main(final String[] argsString) { + extractArguments(argsString); + Arguments arguments = Arguments.getInstance(); + + /* In case of HELP page is requested, + * just print HELP file content and exit application + */ + if (arguments.isHelpRequest()) { + printHelp(System.out); + System.exit(0); + } + try { + // read and initialize twitter configuration + TwitterConfig twitterConfig = new TwitterConfig(); + // define working mode of application + Mode workingMode; + if (arguments.isStreamMode()) { + workingMode = Mode.STREAM; + } else { + workingMode = Mode.QUERY; + } + // get tweets provider for selected working mode and initialize this one + TweetsProvider tweetsProvider = TweetsProviderFactory.getProvider(workingMode); + tweetsProvider.init(twitterConfig); + // get tweet handler for selected working mode + TweetHandler tweetHandler = TweetHandlerFactory.getHandler(workingMode); + // start tweets providing and handling + tweetsProvider.provide(tweetHandler); + } catch (Exception e) { + System.err.println("Application TwitterStream has been occasionally crashed " + + "with error = \"" + e.getMessage() + "\""); + System.err.println("Application will be terminated with error code."); + System.exit(1); + } + } + + /** + * Transforms arguments string to Arguments object with parsed fields. + * @param argsString arguments strings from the input + */ + private static void extractArguments(final String[] argsString) { + JCommander jCommander = new JCommander(); + jCommander.addObject(Arguments.getInstance()); + jCommander.parse(argsString); + } + + /** + * Prints the content of HELP file to the passed output stream. + * @param out stream where HELP page will be printed + */ + private static void printHelp(OutputStream out) { + try { + byte[] buffer = new byte[LINE_LENGTH]; + try (InputStream input = TwitterStream.class.getClassLoader().getResourceAsStream(Constants.HELP_FILE)) { + int length = input.read(buffer); + while (length != -1) { + out.write(buffer, 0, length); + length = input.read(buffer); + } + } + } catch (IOException e) { + System.err.println("Problem with reading help file: \"" + e.getMessage() + "\""); + } + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/config/Arguments.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/config/Arguments.java new file mode 100644 index 0000000..461846a --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/config/Arguments.java @@ -0,0 +1,107 @@ +package config; + +import com.beust.jcommander.Parameter; + +import java.util.List; + +/** + * Arguments storage. + * Used JCommander library. + * + * @see http://jcommander.org/ + */ +public final class Arguments { + private static final Arguments INSTANCE = new Arguments(); + + private Arguments() { } + + public static Arguments getInstance() { + return INSTANCE; + } + + @Parameter(names = {"--query", "-q"}, + required = true, + description = "Query or keywords for stream") + private List keywords; + + @Parameter(names = {"--place", "-p"}, + required = true, + description = "Location for search tweets") + private String place; + + @Parameter(names = {"--stream", "-s"}, + description = "Stream mode when tweets printed with delay") + private boolean streamMode; + + @Parameter(names = "--hideRetweets", + description = "Hides retweets at all") + private boolean hideRetweets; + + @Parameter(names = {"--limit", "-l"}, + description = "Limits the number of printed tweets") + private Integer limitOfTweets = Constants.NO_TWEETS_LIMIT; + + @Parameter(names = {"--help", "-h"}, + help = true, + description = "Requests the help page") + private boolean helpRequest; + + @Parameter(names = {"--verbose", "-v"}, + description = "Verbose mode to print more information") + private boolean verbose; + + /** + * Array of keywords in case of tweets stream requested. + * @return array of keywords to be tracked + */ + public String[] getKeywords() { + String[] keywordsArray = new String[keywords.size()]; + return keywords.toArray(keywordsArray); + } + + /** + * Suppose that query for Search tweets is element with 0 index. + * @return search tweets query + */ + public String getQuery() { + return keywords.get(0); + } + + public String getPlace() { + return place; + } + + public boolean isStreamMode() { + return streamMode; + } + + public boolean hideRetweets() { + return hideRetweets; + } + + public Integer getLimitOfTweets() { + return limitOfTweets; + } + + public boolean isHelpRequest() { + return helpRequest; + } + + public boolean isVerboseMode() { + return verbose; + } + + @Override + public String toString() { + return "Arguments{" + + "keywords=" + keywords + + ", place='" + place + + '\'' + + ", streamMode=" + streamMode + + ", hideRetweets=" + hideRetweets + + ", limitOfTweets=" + limitOfTweets + + ", helpRequest=" + helpRequest + + ", verboseMode=" + verbose + + '}'; + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/config/Constants.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/config/Constants.java new file mode 100644 index 0000000..b9945af --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/config/Constants.java @@ -0,0 +1,31 @@ +package config; + +/** + * Application constants storage. + */ +public final class Constants { + /** + * Reconnect timeout to twitter in seconds . + */ + public static final int RECONNECT_TIMEOUT_SECS = 10; + /** + * Default value of limit argument. + */ + public static final int NO_TWEETS_LIMIT = -1; + /** + * Delay between two printing of tweets. + */ + public static final int PRINT_TWEET_DELAY_SECS = 1; + /** + * Message which is printed in verbose mode when no any tweet to print for stream. + */ + public static final String NO_TWEET_MESSAGE = "..."; + /** + * Name of resource - twitter config file. + */ + public static final String TWITTER_CONFIG_FILE = "twitter.cfg"; + /** + * Name of resource - help content file. + */ + public static final String HELP_FILE = "help.txt"; +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/config/TwitterConfig.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/config/TwitterConfig.java new file mode 100644 index 0000000..9cee397 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/config/TwitterConfig.java @@ -0,0 +1,101 @@ +package config; + +import twitter4j.auth.AccessToken; +import twitter4j.conf.Configuration; +import twitter4j.conf.ConfigurationBuilder; + +import java.io.*; + +/* + * Class loads and holds Twitter Access configuration from file resource + * and provides methods to get {@link twitter4j.conf.Configuration} and {@link twitter4j.auth.AccessToken} + */ +public class TwitterConfig { + private static final String CONSUMER_KEY_PROP_NAME = "consumerKey"; + private static final String CONSUMER_SECRET_PROP_NAME = "consumerSecret"; + private static final String ACCESS_TOKEN_PROP_NAME = "accessToken"; + private static final String ACCESS_TOKEN_SECRET_PROP_NAME = "accessTokenSecret"; + + private String consumerKey; + private String consumerSecret; + private String accessToken; + private String accessTokenSecret; + + public TwitterConfig() { + init(); + } + + private void init() { + File cfgFile = new File(Constants.TWITTER_CONFIG_FILE); + try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(cfgFile)))) { + while (in.ready()) { + String line = in.readLine(); + int indexOfDelimiter = line.indexOf('='); + if (indexOfDelimiter == -1) { + System.err.println("Incorrect line in twitter configuration = '" + line + "'"); + continue; + } + String propName = line.substring(0, indexOfDelimiter); + String propValue = line.substring(indexOfDelimiter + 1, line.length()); + switch (propName) { + case CONSUMER_KEY_PROP_NAME: + consumerKey = propValue; + break; + case CONSUMER_SECRET_PROP_NAME: + consumerSecret = propValue; + break; + case ACCESS_TOKEN_PROP_NAME: + accessToken = propValue; + break; + case ACCESS_TOKEN_SECRET_PROP_NAME: + accessTokenSecret = propValue; + break; + default: + System.err.println("Property '" + propName + "' not recognized"); + } + } + } catch (FileNotFoundException e) { + System.err.println("Twitter config file by path = \"" + cfgFile.getAbsolutePath() + "\" not found"); + } catch (IOException e) { + System.err.println("Problem with reading twitter config file: " + e.getMessage()); + } + + validate(); + } + + private void validate() { + if (consumerKey == null + || consumerSecret == null + || accessToken == null + || accessTokenSecret == null) { + throw new IllegalStateException("Twitter configuration file is incorrect"); + } + } + + public final AccessToken getAccessToken() { + return new AccessToken(accessToken, accessTokenSecret); + } + + public final Configuration getConfiguration() { + ConfigurationBuilder configurationBuilder = new ConfigurationBuilder(). + setOAuthConsumerKey(consumerKey). + setOAuthConsumerSecret(consumerSecret). + setOAuthAccessToken(accessToken). + setOAuthAccessTokenSecret(accessTokenSecret); + return configurationBuilder.build(); + } + + @Override + public final String toString() { + return "TwitterConfig{" + + "consumerKey='" + consumerKey + + '\'' + + ", consumerSecret='" + consumerSecret + + '\'' + + ", accessToken='" + accessToken + + '\'' + + ", accessTokenSecret='" + accessTokenSecret + + '\'' + + '}'; + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/handling/TweetHandler.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/handling/TweetHandler.java new file mode 100644 index 0000000..01d81fc --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/handling/TweetHandler.java @@ -0,0 +1,15 @@ +package core.handling; + +import model.Tweet; + +/** + * Tweet handler interface. + */ +public interface TweetHandler { + + /** + * Handles tweet by any way. + * @param tweet obtained tweet + */ + void handle(Tweet tweet); +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/handling/TweetHandlerFactory.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/handling/TweetHandlerFactory.java new file mode 100644 index 0000000..807bc25 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/handling/TweetHandlerFactory.java @@ -0,0 +1,26 @@ +package core.handling; + +import core.handling.impl.PrintResultOfQueryTweetsHandler; +import core.handling.impl.PrintStreamOfTweetsHandler; +import model.Mode; + +public final class TweetHandlerFactory { + + private TweetHandlerFactory() { } + + /** + * Gets tweets handler depending on the working mode. + * @param mode working mode of application + * @return tweet handler implementation + */ + public static TweetHandler getHandler(Mode mode) { + switch (mode) { + case STREAM: + return new PrintStreamOfTweetsHandler(System.out); + case QUERY: + return new PrintResultOfQueryTweetsHandler(System.out); + default: + throw new IllegalArgumentException("Mode = " + mode + " is not supported"); + } + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/handling/impl/PrintResultOfQueryTweetsHandler.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/handling/impl/PrintResultOfQueryTweetsHandler.java new file mode 100644 index 0000000..b244d2e --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/handling/impl/PrintResultOfQueryTweetsHandler.java @@ -0,0 +1,105 @@ +package core.handling.impl; + +import core.handling.TweetHandler; +import model.Tweet; +import utils.TextUtils; + +import java.io.PrintStream; +import java.util.Date; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * This handler is used in QUERY mode of application. + * It prints tweets every time when new tweet is come. + * + * When new tweet is obtained (method 'handle' invoked) this handler prints + * formatted representation of this new tweet. + */ +public final class PrintResultOfQueryTweetsHandler implements TweetHandler { + private static final int MILLI = 1_000; + private static final int SEC_IN_MIN = 60; + private static final int STRING_SIZE = 256; + + private PrintStream out; + + private AtomicLong tweetCounter = new AtomicLong(0); + + public PrintResultOfQueryTweetsHandler(PrintStream outStream) { + this.out = outStream; + } + + @Override + public void handle(Tweet tweet) { + out.println("Tweet#" + tweetCounter.incrementAndGet() + ":"); + out.println(formatTweet(tweet)); + } + + /* + * Print format is the following: + * + * If tweet IS NOT retweeted + * ---------------------------------------------------------------------------------------- + * [] @: + * ---------------------------------------------------------------------------------------- + * + * If tweet IS retweeted + * ---------------------------------------------------------------------------------------- + * [] @: ретвитнул @: ( ретвитов) + * ---------------------------------------------------------------------------------------- + * + * @param tweet tweet object to be printed + * @return text representation of tweet according to format + */ + private static String formatTweet(Tweet tweet) { + StringBuilder tweetView = new StringBuilder(STRING_SIZE); + tweetView.append("----------------------------------------------------------------------------------------\n"); + if (tweet.isNotRetweet()) { + tweetView.append("[").append(formatTime(tweet.getTime())).append("] "). + append("@").append(getNickname(tweet)). + append(": ").append(tweet.getText()); + } else { + Tweet retweetedTweet = tweet.getRetweetedTweet(); + tweetView.append("[").append(formatTime(tweet.getTime())).append("] "). + append("@").append(getNickname(tweet)). + append(": ретвитнул @").append(getNickname(retweetedTweet)). + append(": ").append(retweetedTweet.getText()). + append(" (").append(retweetedTweet.getRetweetCount()).append(" ретвитов)"); + } + tweetView.append("\n----------------------------------------------------------------------------------------"); + return tweetView.toString(); + } + + private static String getNickname(Tweet tweet) { + String nick = tweet.getAuthor().getName(); + return TextUtils.coloredText(nick, TextUtils.COLOR_BLUE); + } + + /* + * Time format is the following: + * Время должно быть в формате: + * "Только что" - если менее 2х минут назад + * "n минут назад" - если менее часа назад (n - цифрами) + * "n часов назад" - если более часа, но сегодня (n - цифрами) + * "вчера" - если вчера + * "n дней назад" - в остальных случаях (n - цифрами) + * + * @param then the tweet's time in milliseconds + * @return text representation of the tweet's time according to format + */ + private static String formatTime(long then) { + long now = new Date().getTime(); + long diffInMinutes = (now - then) / MILLI / SEC_IN_MIN; + if (diffInMinutes < 2) { + return "Только что"; + } else if (TimeUnit.MINUTES.toHours(diffInMinutes) < 1) { + return diffInMinutes + " минут назад"; + } else if (TimeUnit.MINUTES.toHours(diffInMinutes) >= 1 && TimeUnit.MINUTES.toDays(diffInMinutes) < 1) { + return TimeUnit.MINUTES.toHours(diffInMinutes) + " часов назад"; + } else if (TimeUnit.MINUTES.toDays(diffInMinutes) >= 1 && TimeUnit.MINUTES.toDays(diffInMinutes) < 2) { + return "вчера"; + } else { + return TimeUnit.MINUTES.toDays(diffInMinutes) + " дней назад"; + } + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/handling/impl/PrintStreamOfTweetsHandler.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/handling/impl/PrintStreamOfTweetsHandler.java new file mode 100644 index 0000000..87e6e9a --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/handling/impl/PrintStreamOfTweetsHandler.java @@ -0,0 +1,101 @@ +package core.handling.impl; + +import config.Arguments; +import config.Constants; +import core.handling.TweetHandler; +import model.Tweet; +import utils.TextUtils; + +import java.io.PrintStream; +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicLong; + +/* + * This handler is used in STREAM mode of application. + * It prints tweets every Constants.PRINT_TWEET_DELAY_SECS (by default every 1 second). + * + * When new tweet is obtained (method 'handle' invoked) this handler stores this tweet + * to internal queue. When next print step is come, handler takes tweet from queue + * (by 'poll' method) and prints this tweet. + * In case when no available tweet in queue, it will print nothing or + * Constants.NO_TWEET_MESSAGE in VERBOSE mode. + */ +public final class PrintStreamOfTweetsHandler implements TweetHandler { + public static final int STRING_SIZE = 256; + public static final int KILO = 1_000; + private PrintStream out; + private Queue tweetQueue; + + private boolean started = false; + private AtomicLong tweetCounter = new AtomicLong(0); + + public PrintStreamOfTweetsHandler(final PrintStream outStream) { + this.out = outStream; + tweetQueue = new ArrayDeque<>(); + } + + @Override + public void handle(Tweet tweet) { + tweetQueue.offer(tweet); + + if (!started) { + // schedule timer with task of printing tweets from queue + new Timer().schedule(new TimerTask() { + @Override + public void run() { + Tweet next = tweetQueue.poll(); + if (next != null) { + out.println("Tweet#" + tweetCounter.incrementAndGet() + ":"); + out.println(formatTweet(next)); + } else { + if (Arguments.getInstance().isVerboseMode()) { + out.println(Constants.NO_TWEET_MESSAGE); + } + } + } + }, 0, Constants.PRINT_TWEET_DELAY_SECS * KILO); + started = true; + } + } + + /* + * Print format is the following: + * + * If tweet IS NOT retweeted + * ---------------------------------------------------------------------------------------- + * @: + * ---------------------------------------------------------------------------------------- + * + * If tweet IS retweeted + * ---------------------------------------------------------------------------------------- + * @: ретвитнул @: ( ретвитов) + * ---------------------------------------------------------------------------------------- + * + * @param tweet tweet object to be printed + * @return text representation of tweet according to format + */ + private static String formatTweet(Tweet tweet) { + StringBuilder tweetView = new StringBuilder(STRING_SIZE); + tweetView.append("----------------------------------------------------------------------------------------\n"); + if (tweet.isNotRetweet()) { + tweetView.append("@").append(getNickname(tweet)). + append(": ").append(tweet.getText()); + } else { + Tweet retweetedTweet = tweet.getRetweetedTweet(); + tweetView.append("@").append(getNickname(tweet)). + append(": ретвитнул @").append(getNickname(retweetedTweet)). + append(": ").append(retweetedTweet.getText()). + append(" (").append(retweetedTweet.getRetweetCount()).append(" ретвитов)"); + } + tweetView.append("\n----------------------------------------------------------------------------------------"); + return tweetView.toString(); + } + + private static String getNickname(Tweet tweet) { + String nick = tweet.getAuthor().getName(); + return TextUtils.coloredText(nick, TextUtils.COLOR_BLUE); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/providing/TweetsProvider.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/providing/TweetsProvider.java new file mode 100644 index 0000000..e56766a --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/providing/TweetsProvider.java @@ -0,0 +1,14 @@ +package core.providing; + +import config.TwitterConfig; +import core.handling.TweetHandler; + +/** + * Tweets provider interface. + */ +public interface TweetsProvider { + + void init(TwitterConfig twitterConfig); + + void provide(TweetHandler handler); +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/providing/TweetsProviderFactory.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/providing/TweetsProviderFactory.java new file mode 100644 index 0000000..664dd46 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/providing/TweetsProviderFactory.java @@ -0,0 +1,29 @@ +package core.providing; + +import core.providing.impl.TweetsByQueryProvider; +import core.providing.impl.TweetsStreamProvider; +import model.Mode; + +/** + * Tweets provider factory. + */ +public final class TweetsProviderFactory { + + private TweetsProviderFactory() { } + + /** + * Gets tweets provider depending on the working mode. + * @param mode working mode of application + * @return tweets provider implementation + */ + public static TweetsProvider getProvider(Mode mode) { + switch (mode) { + case STREAM: + return new TweetsStreamProvider(); + case QUERY: + return new TweetsByQueryProvider(); + default: + throw new IllegalArgumentException("Mode = " + mode + " is not supported"); + } + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/providing/impl/TweetsByQueryProvider.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/providing/impl/TweetsByQueryProvider.java new file mode 100644 index 0000000..ba200c3 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/providing/impl/TweetsByQueryProvider.java @@ -0,0 +1,65 @@ +package core.providing.impl; + +import config.Arguments; +import config.Constants; +import config.TwitterConfig; +import core.handling.TweetHandler; +import core.providing.TweetsProvider; +import core.quering.SearchQueryBuilder; +import model.Tweet; +import twitter4j.*; + +import java.util.concurrent.TimeUnit; + +/** + * Tweets by query provider, provides requested tweets to handler. + * + * Query is built by SearchQueryBuilder and method 'search' of + * Twitter instance is invoked. + * + * QueryResult contains list of statuses, which transformed to + * internal Tweet objects, after that these tweets are sent to + * handler. + */ +public final class TweetsByQueryProvider implements TweetsProvider { + private Twitter twitter; + private Query query; + + @Override + public void init(TwitterConfig twitterConfig) { + twitter = new TwitterFactory(twitterConfig.getConfiguration()).getInstance(); + query = new SearchQueryBuilder(twitter).buildQuery(); + } + + @Override + public void provide(TweetHandler handler) { + try { + QueryResult result = twitter.search(query); + if (result.getTweets() == null || result.getTweets().isEmpty()) { + System.out.println("No any tweets by specified query and place found"); + } + for (Status status : result.getTweets()) { + // transform status to Tweet object and send to handler + Tweet tweet = Tweet.valueOf(status); + if (Arguments.getInstance().hideRetweets() && tweet.isRetweet()) { + // skip this tweet + continue; + } + handler.handle(tweet); + } + } catch (TwitterException e) { + System.err.println("Twitter has been occasionally crashed with error: \"" + e.getMessage() + "\""); + System.err.println("Try one more time... [timeout = " + Constants.RECONNECT_TIMEOUT_SECS + " secs]"); + timeout(Constants.RECONNECT_TIMEOUT_SECS); + provide(handler); + } + } + + private static void timeout(int seconds) { + try { + TimeUnit.SECONDS.sleep(seconds); + } catch (InterruptedException e) { + // do nothing + } + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/providing/impl/TweetsStreamProvider.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/providing/impl/TweetsStreamProvider.java new file mode 100644 index 0000000..cc4e514 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/providing/impl/TweetsStreamProvider.java @@ -0,0 +1,96 @@ +package core.providing.impl; + +import config.Arguments; +import config.Constants; +import config.TwitterConfig; +import core.handling.TweetHandler; +import core.providing.TweetsProvider; +import core.quering.FilterQueryBuilder; +import model.Tweet; +import twitter4j.*; +import twitter4j.auth.AccessToken; +import twitter4j.conf.Configuration; + +import java.util.concurrent.TimeUnit; + +/** + * Tweets stream provider, provides tweets to TweetHandler. + * + * This class implements StatusAdapter and registers itself + * as StatusListener in TwitterStream. + * Every time when new status obtained the transformation to + * internal Tweet object performed, after that this Tweet sent + * to the handler. + */ +public final class TweetsStreamProvider extends StatusAdapter implements TweetsProvider { + private Configuration configuration; + private AccessToken accessToken; + + private TwitterStream twitterStream; + private FilterQuery filterQuery; + + private TweetHandler tweetHandler; + + @Override + public void init(TwitterConfig twitterConfig) { + configuration = twitterConfig.getConfiguration(); + accessToken = twitterConfig.getAccessToken(); + } + + @Override + public void provide(TweetHandler handler) { + this.tweetHandler = handler; + connect(); + } + + @Override + public void onStatus(Status status) { + Tweet tweet = Tweet.valueOf(status); + if (Arguments.getInstance().hideRetweets() && tweet.isRetweet()) { + // skip this tweet + return; + } + tweetHandler.handle(tweet); + } + + @Override + public void onException(Exception e) { + System.err.println("Twitter stream has been occasionally crashed with error: \"" + e.getMessage() + "\""); + System.err.println("Try to reconnect... [timeout = " + Constants.RECONNECT_TIMEOUT_SECS + " secs]"); + reconnect(); + } + + private void connect() { + twitterStream = new TwitterStreamFactory(configuration).getInstance(accessToken); + Twitter twitter = new TwitterFactory(configuration).getInstance(); + filterQuery = new FilterQueryBuilder(twitter).buildQuery(); + // register itself as status listener + // method 'onStatus' will execute every time when new tweet obtained + twitterStream.addListener(this); + twitterStream.filter(filterQuery); + } + + private void disconnect() { + if (twitterStream != null) { + twitterStream.cleanUp(); + twitterStream.shutdown(); + // help GC + twitterStream = null; + } + } + + private void reconnect() { + // reconnect is performed with timeout between disconnect and new connect + disconnect(); + timeout(Constants.RECONNECT_TIMEOUT_SECS); + connect(); + } + + private static void timeout(int seconds) { + try { + TimeUnit.SECONDS.sleep(seconds); + } catch (InterruptedException e) { + // do nothing + } + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/quering/FilterQueryBuilder.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/quering/FilterQueryBuilder.java new file mode 100644 index 0000000..1b13f1a --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/quering/FilterQueryBuilder.java @@ -0,0 +1,55 @@ +package core.quering; + +import config.Arguments; +import utils.GeoUtils; +import twitter4j.*; + +/** + * FilterQuery builder to build query for Twitter searching + * based on arguments (query, place). + */ +public class FilterQueryBuilder { + private Twitter twitter; + + public FilterQueryBuilder(Twitter twiter) { + this.twitter = twiter; + } + + public final FilterQuery buildQuery() { + Arguments arguments = Arguments.getInstance(); + + FilterQuery query = new FilterQuery(); + query.track(arguments.getKeywords()); + + // calculate and set place for tweets + try { + Place place = GeoUtils.findPlaceByName(twitter, arguments.getPlace()); + GeoLocation[] vertices = place.getBoundingBoxCoordinates()[0]; + /* + * We try to find two points of box + * - the first with MIN latitude and longitude + * - the second with MAX latitude and longitude + * These two points will be used as filtering location for tweets. + * + * For additional details see: + * https://dev.twitter.com/streaming/overview/request-parameters#locations + * Checked with "New York City" ({-74,40},{-73,41}) + */ + double minLongitude = Double.MAX_VALUE, minLatitude = Double.MAX_VALUE; + double maxLongitude = -Double.MAX_VALUE, maxLatitude = -Double.MAX_VALUE; + for (GeoLocation vertex : vertices) { + minLongitude = Math.min(minLongitude, vertex.getLongitude()); + minLatitude = Math.min(minLatitude, vertex.getLatitude()); + maxLongitude = Math.max(maxLongitude, vertex.getLongitude()); + maxLatitude = Math.max(maxLatitude, vertex.getLatitude()); + } + double[][] locations = {{minLongitude, minLatitude}, {maxLongitude, maxLatitude}}; + query.locations(locations); + } catch (TwitterException te) { + System.err.println("Searching of places has been crashed with error = \"" + te.getMessage() + "\""); + System.err.println("Search query will be created without place condition."); + } + + return query; + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/quering/SearchQueryBuilder.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/quering/SearchQueryBuilder.java new file mode 100644 index 0000000..3948ad3 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/core/quering/SearchQueryBuilder.java @@ -0,0 +1,94 @@ +package core.quering; + +import config.Arguments; +import config.Constants; +import utils.GeoUtils; +import twitter4j.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * SearchQuery builder to build query for tweets streaming + * based on arguments (keywords, place). + */ +public class SearchQueryBuilder { + private Twitter twitter; + + public SearchQueryBuilder(Twitter twiter) { + this.twitter = twiter; + } + + public final Query buildQuery() { + Arguments arguments = Arguments.getInstance(); + Query query = new Query(); + // set query for tweets + query.setQuery(arguments.getQuery()); + // set limit of tweets if specified in arguments + if (arguments.getLimitOfTweets() != Constants.NO_TWEETS_LIMIT) { + query.setCount(arguments.getLimitOfTweets()); + } + // calculate and set place for tweets + try { + Place place = GeoUtils.findPlaceByName(twitter, arguments.getPlace()); + GeoLocation[] vertices = place.getBoundingBoxCoordinates()[0]; + /* + * Implemented approach which described in the task: + * + * Для Twitter.search использовать среднее арифметическое широты и долготы + * Place.getBoundingBoxCoordinates() и радиус как половину максимального + * расстояния между точками. + */ + GeoLocation center = getCenter(vertices); + double radius = getRadius(vertices); + query.setGeoCode(center, radius, Query.Unit.mi); + } catch (TwitterException te) { + System.err.println("Searching of places has been crashed with error = \"" + te.getMessage() + "\""); + System.err.println("Search query will be created without place condition."); + } + + return query; + } + + public static GeoLocation getCenter(GeoLocation[] vertices) { + double centerLatitude, centerLongitude; + double[] latitudes = new double[vertices.length]; + double[] longitudes = new double[vertices.length]; + for (int i = 0; i < vertices.length; i++) { + GeoLocation vertex = vertices[i]; + latitudes[i] = vertex.getLatitude(); + longitudes[i] = vertex.getLongitude(); + } + centerLatitude = getArithmeticMean(latitudes); + centerLongitude = getArithmeticMean(longitudes); + return new GeoLocation(centerLatitude, centerLongitude); + } + + public static double getRadius(GeoLocation[] vertices) { + List distances = new ArrayList<>(); + // calculate distances between all vertices + for (int i = 0; i < vertices.length - 1; i++) { + for (int j = i + 1; j < vertices.length; j++) { + GeoLocation vertex1 = vertices[i]; + GeoLocation vertex2 = vertices[j]; + distances.add(GeoUtils.distanceBetweenTwoCoordinates( + vertex1.getLatitude(), vertex1.getLongitude(), + vertex2.getLatitude(), vertex2.getLongitude()) + ); + } + } + // sort list in reverse order (from MAX to MIN) + // and get the first - MAX of all distances + Collections.sort(distances, Collections.reverseOrder()); + return distances.get(0); + } + + public static double getArithmeticMean(double[] numbers) { + double sum = 0; + for (double num : numbers) { + sum += num; + } + return sum / numbers.length; + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/model/Mode.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/model/Mode.java new file mode 100644 index 0000000..23dc708 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/model/Mode.java @@ -0,0 +1,8 @@ +package model; + +/** + * Working mode of application - as tweets stream or tweets by query. + */ +public enum Mode { + STREAM, QUERY; +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/model/Tweet.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/model/Tweet.java new file mode 100644 index 0000000..9ead43c --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/model/Tweet.java @@ -0,0 +1,80 @@ +package model; + +import twitter4j.Status; + +/** + * Class represents Tweet object. + */ +public final class Tweet { + private String text; + private TwitterUser author; + private long time; + private long retweetCount; + private Tweet retweetedTweet; + + public String getText() { + return text; + } + + public void setText(String tweetText) { + this.text = tweetText; + } + + public TwitterUser getAuthor() { + return author; + } + + public void setAuthor(TwitterUser tweetAuthor) { + this.author = tweetAuthor; + } + + public long getRetweetCount() { + return retweetCount; + } + + public void setRetweetCount(long count) { + this.retweetCount = count; + } + + public long getTime() { + return time; + } + + public void setTime(long tweetTime) { + this.time = tweetTime; + } + + public Tweet getRetweetedTweet() { + return retweetedTweet; + } + + public void setRetweetedTweet(Tweet retweeted) { + this.retweetedTweet = retweeted; + } + + public boolean isRetweet() { + return null != retweetedTweet; + } + + public boolean isNotRetweet() { + return !isRetweet(); + } + + /** + * Factory method to convert twitter4j.Status object to internal model - Tweet object. + * @param twitter4jStatus twitter4j.Status object + * @return Tweet object + */ + public static Tweet valueOf(Status twitter4jStatus) { + Tweet tweet = new Tweet(); + if (twitter4jStatus.getRetweetedStatus() != null) { + tweet.setRetweetedTweet(valueOf(twitter4jStatus.getRetweetedStatus())); + } + tweet.setAuthor(TwitterUser.valueOf(twitter4jStatus.getUser())); + tweet.setText(twitter4jStatus.getText()); + tweet.setTime(twitter4jStatus.getCreatedAt().getTime()); + tweet.setRetweetCount(twitter4jStatus.getRetweetCount()); + + return tweet; + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/model/TwitterUser.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/model/TwitterUser.java new file mode 100644 index 0000000..a10e491 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/model/TwitterUser.java @@ -0,0 +1,42 @@ +package model; + +import twitter4j.User; + +/** + * Class represents Twitter User object. + */ +public class TwitterUser { + private Long id; + private String name; + + public TwitterUser(Long userid, String username) { + this.id = userid; + this.name = username; + } + + public final Long getId() { + return id; + } + + public final String getName() { + return name; + } + + /** + * Factory method to convert twitter4j.User object to internal model - TwitterUser object. + * @param twitter4jUser twitter4j.User object + * @return TwitterUser object + */ + public static TwitterUser valueOf(User twitter4jUser) { + return new TwitterUser(twitter4jUser.getId(), twitter4jUser.getName()); + } + + @Override + public final String toString() { + return "TwitterUser{" + + "id=" + id + + ", name='" + name + + '\'' + + '}'; + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/utils/GeoUtils.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/utils/GeoUtils.java new file mode 100644 index 0000000..662507b --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/utils/GeoUtils.java @@ -0,0 +1,51 @@ +package utils; + +import twitter4j.*; + +import static java.lang.Math.*; + +public final class GeoUtils { + public static final int CONST1 = 60; + public static final double CONST2 = 1.1515; + + private GeoUtils() { } + + /* + * Finds twitter4j.Place object by place name. + * @param twitter initialized twitter instance + * @param placeName name of place to search + * @return found twitter4j.Place object + * @throws TwitterException + */ + public static Place findPlaceByName(Twitter twitter, String placeName) throws TwitterException { + GeoQuery geoQuery = new GeoQuery((String) null); + geoQuery.setQuery(placeName); + // we need the only one place + geoQuery.setMaxResults(1); + ResponseList places = twitter.searchPlaces(geoQuery); + if (places.isEmpty()) { + throw new IllegalArgumentException("Place by name = '" + placeName + "' not found"); + } + return places.get(0); + } + + /* + * Calculates distance between two points based on their longitude and latitude. + * @see + * + * @param lat1 latitude of point_1 + * @param lon1 longitude of point_1 + * @param lat2 latitude of point_2 + * @param lon2 longitude of point_2 + * @return distance in miles + */ + public static double distanceBetweenTwoCoordinates(double lat1, double lon1, double lat2, double lon2) { + double theta = lon1 - lon2; + double dist = sin(toRadians(lat1)) * sin(toRadians(lat2)) + + cos(toRadians(lat1)) * cos(toRadians(lat2)) * cos(toRadians(theta)); + dist = acos(dist); + dist = toDegrees(dist); + dist = dist * CONST1 * CONST2; + return dist; + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/utils/TextUtils.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/utils/TextUtils.java new file mode 100644 index 0000000..e7f9304 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/java/utils/TextUtils.java @@ -0,0 +1,16 @@ +package utils; + +public final class TextUtils { + public static final String COLOR_RESET = "\u001B[0m"; + public static final String COLOR_BLUE = "\u001B[34m"; + + // to prevent instantiating + // this class must be used as static only + private TextUtils() { } + + public static String coloredText(String text, String color) { + return color + + text + + COLOR_RESET; + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/resources/help.txt b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/resources/help.txt new file mode 100644 index 0000000..95008c4 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/src/main/resources/help.txt @@ -0,0 +1,11 @@ +TwitterStream - it's a console application to print the stream of tweets onto the screen +according to the specified conditions. + +Command usage: +java TwitterStream \ + [--query|-q ] \ + [--place|-p ] \ + [--stream|-s] \ + [--hideRetweets] \ + [--limit|-l ] \ + [--help|-h] diff --git "a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/~$\320\260\321\202\320\272\320\260\321\217 \320\270\320\275\321\201\321\202\321\200\321\203\320\272\321\206\320\270\321\217.txt" "b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/~$\320\260\321\202\320\272\320\260\321\217 \320\270\320\275\321\201\321\202\321\200\321\203\320\272\321\206\320\270\321\217.txt" new file mode 100644 index 0000000..0ebf8fb Binary files /dev/null and "b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/TwitterStream/~$\320\260\321\202\320\272\320\260\321\217 \320\270\320\275\321\201\321\202\321\200\321\203\320\272\321\206\320\270\321\217.txt" differ diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/pom.xml b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/pom.xml new file mode 100644 index 0000000..9194c51 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + collection-ql + collection-ql + 1.0 + + + + org.apache.commons + commons-collections4 + 4.0 + + + + junit + junit + 4.4 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.8 + 1.8 + + + + + \ No newline at end of file diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/CollectionQuery.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/CollectionQuery.java new file mode 100644 index 0000000..e3f5374 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/CollectionQuery.java @@ -0,0 +1,43 @@ +import client.Statistics; +import client.Student; +import library.core.exceptions.IncorrectQueryException; + +import java.time.LocalDate; + +import static client.Student.student; +import static library.api.Aggregates.avg; +import static library.api.Aggregates.constant; +import static library.api.Aggregates.count; +import static library.api.Conditions.like; +import static library.api.OrderByConditions.asc; +import static library.api.OrderByConditions.desc; +import static library.api.Sources.from; +import static library.api.Sources.list; + +public class CollectionQuery { + + public static void main(String[] args) throws IncorrectQueryException { + final int const10 = 10; + final int const100 = 100; + Iterable statistics = + from(list( + student("ivanov", LocalDate.parse("1986-08-06"), "494"), + student("sidorov", LocalDate.parse("1986-08-06"), "495"), + student("smith", LocalDate.parse("1986-08-06"), "495"), + student("petrov", LocalDate.parse("1996-08-06"), "494"))) + .select(Statistics.class, Student::getGroup, count(Student::getGroup), avg(Student::age)) + .where(like(Student::getName, ".*ov").and(s -> s.age() > const10)) + .groupBy(Student::getGroup) + .having(s -> s.getCount() > 0) + .orderBy(asc(Statistics::getGroup), desc(Statistics::getCount)) + .limit(const100) + .union( + from(list(student("ivanov", LocalDate.parse("1985-08-06"), "494"))) + .selectDistinct(Statistics.class, constant("all"), count(s -> 1), + avg(Student::age)) + ) + .execute(); + System.out.println(statistics); + } + +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/client/Statistics.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/client/Statistics.java new file mode 100644 index 0000000..d41b6d5 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/client/Statistics.java @@ -0,0 +1,65 @@ +package client; + +public class Statistics { + + private final String group; + private final Long count; + private final Long age; + private final int const1 = 31; + + public final String getGroup() { + return group; + } + + public final Long getCount() { + return count; + } + + public final Long getAge() { + return age; + } + + public Statistics(String group1, Long count1, Long age1) { + this.group = group1; + this.count = count1; + this.age = age1; + } + + @Override + public final boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Statistics that = (Statistics) o; + + if (!group.equals(that.group)) { + return false; + } + if (!count.equals(that.count)) { + return false; + } + return age.equals(that.age); + + } + + @Override + public final int hashCode() { + int result = group.hashCode(); + result = const1 * result + count.hashCode(); + result = const1 * result + age.hashCode(); + return result; + } + + @Override + public final String toString() { + return "Statistics{" + + "group=" + group + + ", count=" + count + + ", avg=" + age + + '}'; + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/client/Student.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/client/Student.java new file mode 100644 index 0000000..b585e54 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/client/Student.java @@ -0,0 +1,39 @@ +package client; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +public class Student { + private final String name; + + private final LocalDate dateOfBirth; + + private final String group; + + public final String getName() { + return name; + } + + public Student(String name1, LocalDate dateOfBirth1, String group1) { + this.name = name1; + this.dateOfBirth = dateOfBirth1; + this.group = group1; + } + + public final LocalDate getDateOfBirth() { + return dateOfBirth; + } + + public final String getGroup() { + return group; + } + + public final long age() { + return ChronoUnit.YEARS.between(getDateOfBirth(), LocalDateTime.now()); + } + + public static Student student(String name1, LocalDate dateOfBirth1, String group1) { + return new Student(name1, dateOfBirth1, group1); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/client/StudentInfo.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/client/StudentInfo.java new file mode 100644 index 0000000..bb9a4fd --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/client/StudentInfo.java @@ -0,0 +1,66 @@ +package client; + +public class StudentInfo { + private String name; + private String group; + private long age; + + public StudentInfo(String name1, String group1, Long age1) { + this.name = name1; + this.group = group1; + this.age = age1; + } + + public final String getName() { + return name; + } + + public final String getGroup() { + return group; + } + + public final long getAge() { + return age; + } + + @Override + public final boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + StudentInfo that = (StudentInfo) o; + + if (age != that.age) { + return false; + } + if (!name.equals(that.name)) { + return false; + } + return group.equals(that.group); + + } + + @Override + public final int hashCode() { + final int const31 = 31; + final int const32 = 32; + + int result = name.hashCode(); + result = const31 * result + group.hashCode(); + result = const31 * result + (int) (age ^ (age >>> const32)); + return result; + } + + @Override + public final String toString() { + return "StudentInfo{" + + "name=" + name + + ", group=" + group + + ", age=" + age + + '}'; + } + } diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/Aggregates.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/Aggregates.java new file mode 100644 index 0000000..73e65ef --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/Aggregates.java @@ -0,0 +1,50 @@ +package library.api; + +import library.core.model.aggregation.AggregateFunction; +import library.core.model.aggregation.impl.AverageFunction; +import library.core.model.aggregation.impl.CountFunction; +import library.core.model.aggregation.impl.MaxFunction; +import library.core.model.aggregation.impl.MinFunction; + +import java.util.function.Function; + +/** + * Aggregate functions. + */ +public class Aggregates { + + public static AggregateFunction count(Function countingFunction) { + return new CountFunction<>(countingFunction); + } + + public static AggregateFunction avg(Function averageFunction) { + return new AverageFunction<>(averageFunction); + } + + public static AggregateFunction max(Function function) { + return new MaxFunction<>(function); + } + + public static AggregateFunction min(Function function) { + return new MinFunction<>(function); + } + + /** + * This function represents AggregateFunction stub and + * can be used in aggregate select statement. + * Provides always the same value - constant argument. + * + * @param constant to provide value + * @param source element type + * @param result element type + * @return function stub for constant value + */ + public static AggregateFunction constant(R constant) { + return new AggregateFunction(e -> constant) { + @Override + public R apply(Iterable elements) { + return constant; + } + }; + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/Conditions.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/Conditions.java new file mode 100644 index 0000000..fd56eea --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/Conditions.java @@ -0,0 +1,25 @@ +package library.api; + +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * "where" statement conditions. + */ +public class Conditions { + + public static Predicate like(Function source, String mask) { + Objects.requireNonNull(source); + Objects.requireNonNull(mask); + return t -> { + Objects.requireNonNull(t); + String actual = source.apply(t); + return actual.matches(mask.replace("%", ".*")); + }; + } + + public static Predicate not(Predicate original) { + return original.negate(); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/OrderByConditions.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/OrderByConditions.java new file mode 100644 index 0000000..7c72bec --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/OrderByConditions.java @@ -0,0 +1,18 @@ +package library.api; + +import java.util.Comparator; +import java.util.function.Function; + +/* + * "orderBy" helpers - comparators with asc and desc order based on lambda function + */ +public class OrderByConditions { + + public static Comparator asc(Function condition) { + return (o1, o2) -> condition.apply(o1).compareTo(condition.apply(o2)); + } + + public static Comparator desc(Function condition) { + return (o1, o2) -> condition.apply(o2).compareTo(condition.apply(o1)); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/Query.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/Query.java new file mode 100644 index 0000000..b406532 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/Query.java @@ -0,0 +1,29 @@ +package library.api; + +import library.core.exceptions.IncorrectQueryException; + +import java.util.Comparator; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * Query builder class. + * @param result element type + * @param source element type + */ +public interface Query { + + Query where(Predicate whereCondition); + + Query groupBy(Function... groupByFunction); + + Query having(Predicate havingCondition); + + Query orderBy(Comparator... orderByComparators); + + Query limit(int limit); + + Query union(Query query); + + Iterable execute() throws IncorrectQueryException; +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/Source.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/Source.java new file mode 100644 index 0000000..e00ec44 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/Source.java @@ -0,0 +1,10 @@ +package library.api; + +import java.util.function.Function; + +public interface Source { + + Query select(Class resultClass, Function... arguments); + + Query selectDistinct(Class resultClass, Function... arguments); +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/Sources.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/Sources.java new file mode 100644 index 0000000..13fa126 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/api/Sources.java @@ -0,0 +1,21 @@ +package library.api; + +import library.core.QuerySource; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * "from" statement sources. + */ +public class Sources { + + public static List list(T... elements) { + return new ArrayList<>(Arrays.asList(elements)); + } + + public static Source from(List list) { + return new QuerySource<>(list); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/QueryContext.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/QueryContext.java new file mode 100644 index 0000000..bf3a3f6 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/QueryContext.java @@ -0,0 +1,167 @@ +package library.core; + +import library.api.Query; +import library.core.model.GroupingCondition; +import library.core.model.SelectArgument; + +import java.util.*; +import java.util.function.Predicate; + +/** + * The most important class, which contains all information about executed query. + * + * This context is piped from one operation to another and shared between all operations. + * It plays the role of shared storage on the every step. + * + * @param result type + * @param source element type + */ +public final class QueryContext { + /** + * Source elements of query ("from" statement). + */ + private List source; + /** + * Result class in "select" statement. + */ + private Class resultClass; + /** + * Select arguments in "select" statement. + */ + private List> selectArguments; + /** + * Whether distinct select requested or not. + */ + private boolean distinct; + /** + * Predicate in "where" statement. + */ + private Predicate where; + /** + * Grouping conditions in "groupBy" statement. + */ + private List> groupingConditions; + /** + * Predicate in "having" statement. + */ + private Predicate having; + /** + * Ordered comparators in "orderBy" statement. + */ + private Comparator[] orderBy; + /** + * Limit of result rows. + */ + private int limit; + /** + * Joined queries by "union" statement. + */ + private LinkedList> unions; + /** + * Result of query execution. + */ + private LinkedList result; + + public List getSource() { + return source; + } + + public void setSource(List source1) { + this.source = source1; + } + + public Class getResultClass() { + return resultClass; + } + + public void setResultClass(Class resultClass1) { + this.resultClass = resultClass1; + } + + + public List> getSelectArguments() { + return selectArguments; + } + + public void setSelectArguments(List> selectArguments1) { + this.selectArguments = selectArguments1; + } + + public boolean isDistinct() { + return distinct; + } + + public void setDistinct(boolean distinct1) { + this.distinct = distinct1; + } + + public Predicate getWhere() { + return where; + } + + public void setWhere(Predicate where1) { + this.where = where1; + } + + public List> getGroupingConditions() { + return groupingConditions; + } + + public void setGroupingConditions(List> groupingConditions1) { + this.groupingConditions = groupingConditions1; + } + + public Predicate getHaving() { + return having; + } + + public void setHaving(Predicate having1) { + this.having = having1; + } + + public Comparator[] getOrderBy() { + return orderBy; + } + + public void setOrderBy(Comparator[] orderBy1) { + this.orderBy = orderBy1; + } + + public int getLimit() { + return limit; + } + + public void setLimit(int limit1) { + this.limit = limit1; + } + + public List> getUnions() { + return unions; + } + + public void addUnion(Query union) { + if (unions == null) { + this.unions = new LinkedList<>(); + } + unions.add(union); + } + + public List getResult() { + return result; + } + + public void setResult(List result1) { + this.result = new LinkedList<>(result1); + } + + public void addResult(List result1) { + if (this.result == null) { + this.result = new LinkedList<>(); + } + this.result.addAll(0, result1); + } + + public boolean isGroupingQuery() { + return groupingConditions != null; + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/QueryImpl.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/QueryImpl.java new file mode 100644 index 0000000..d5a6130 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/QueryImpl.java @@ -0,0 +1,129 @@ +package library.core; + +import library.api.Query; +import library.core.exceptions.IncorrectQueryException; +import library.core.model.GroupingCondition; +import library.core.operations.OperationType; +import library.core.operations.QueryOperation; +import library.core.operations.QueryOperationFactory; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * Class implements all functionality of Query interface. + * + * The main idea is: + * for each query statement the arguments are set to QueryContext + * and the corresponding operation is included in QueryOperationsChain. + * + * When method "execute" invoked all operations are ordered by + * their priority and executed bit by bit by using common Query context. + * + * @param result element type + * @param source element type + */ +public final class QueryImpl implements Query { + private final QueryContext queryContext; + private final List queryOperationsChain; + + public QueryImpl(QueryContext queryContext1) { + Objects.requireNonNull(queryContext1); + this.queryContext = queryContext1; + this.queryOperationsChain = new LinkedList<>(); + } + + @Override + public Query where(Predicate whereCondition) { + Objects.requireNonNull(whereCondition); + queryContext.setWhere(whereCondition); + queryOperationsChain.add(QueryOperationFactory.getOperationByType(OperationType.WHERE_OP)); + return this; + } + + @Override + public Query groupBy(Function... groupByConditions) { + if (groupByConditions.length == 0) { + throw new IllegalArgumentException("GroupBy statement without condition(-s)"); + } + List> groupingConditions = + IntStream.range(0, groupByConditions.length) + .mapToObj(i -> new GroupingCondition<>(i, groupByConditions[i])) + .sorted((a1, a2) -> (Integer.compare(a1.getOrder(), a2.getOrder()))) + .collect(Collectors.toList()); + queryContext.setGroupingConditions(groupingConditions); + return this; + } + + @Override + public Query having(Predicate havingCondition) { + Objects.requireNonNull(havingCondition); + queryContext.setHaving(havingCondition); + queryOperationsChain.add(QueryOperationFactory.getOperationByType(OperationType.HAVING_OP)); + return this; + } + + @Override + public Query orderBy(Comparator... orderByComparators) { + if (orderByComparators.length == 0) { + throw new IllegalArgumentException("OrderBy statement without comparator(-s)"); + } + queryContext.setOrderBy(orderByComparators); + queryOperationsChain.add(QueryOperationFactory.getOperationByType(OperationType.ORDER_BY_OP)); + return this; + } + + @Override + public Query limit(int limit) { + if (limit <= 0) { + throw new IllegalArgumentException("limit must be positive"); + } + queryContext.setLimit(limit); + queryOperationsChain.add(QueryOperationFactory.getOperationByType(OperationType.LIMIT_OP)); + return this; + } + + @Override + public Query union(Query query) { + Objects.requireNonNull(query); + queryContext.addUnion(query); + QueryOperation unionOp = QueryOperationFactory.getOperationByType(OperationType.UNION_OP); + if (!queryOperationsChain.contains(unionOp)) { + queryOperationsChain.add(unionOp); + } + return this; + } + + @Override + public Iterable execute() throws IncorrectQueryException { + // here will be fun ! + + // add select operation to chain of query operations + QueryOperation selectOperation; + if (queryContext.isGroupingQuery()) { + selectOperation = QueryOperationFactory.getOperationByType(OperationType.GROUPING_SELECT_OP); + } else { + selectOperation = QueryOperationFactory.getOperationByType(OperationType.SIMPLE_SELECT_OP); + } + + queryOperationsChain.add(selectOperation); + if (queryContext.isDistinct()) { + queryOperationsChain.add(QueryOperationFactory.getOperationByType(OperationType.DISTINCT_OP)); + } + + // order operations by their order number + Collections.sort(queryOperationsChain, QueryOperation.ORDER_COMPARATOR); + // validate every statement before execution + for (QueryOperation op : queryOperationsChain) { + op.validate(queryContext); + } + // execute every operation step by step + for (QueryOperation op : queryOperationsChain) { + op.execute(queryContext); + } + return queryContext.getResult(); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/QuerySource.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/QuerySource.java new file mode 100644 index 0000000..0cb195e --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/QuerySource.java @@ -0,0 +1,69 @@ +package library.core; + +import library.api.Query; +import library.api.Source; +import library.core.model.SelectArgument; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * Class represents the head point of query building. + * + * To start build query a user must create QuerySource object + * and invoke select[Distinct] method to get Query object. + * + * When select[Distinct] method invoked, it creates new + * QueryContext and Query object, fills it by "select" statement + * parameters and return Query instance to requester. + * + * @param source element type + */ +public final class QuerySource implements Source { + private List sourceList; + + public QuerySource(List sourceList1) { + Objects.requireNonNull(sourceList1); + if (sourceList1.isEmpty()) { + throw new IllegalArgumentException("Source collection must not be null"); + } + this.sourceList = sourceList1; + } + + @Override + public Query select(Class resultClass, Function... arguments) { + QueryContext queryContext = buildQueryContext(resultClass, arguments); + queryContext.setDistinct(false); + return new QueryImpl<>(queryContext); + } + + @Override + public Query selectDistinct(Class resultClass, Function... arguments) { + QueryContext queryContext = buildQueryContext(resultClass, arguments); + queryContext.setDistinct(true); + return new QueryImpl<>(queryContext); + } + + private QueryContext buildQueryContext(Class resultClass, Function... arguments) { + Objects.requireNonNull(resultClass); + if (arguments.length == 0) { + throw new IllegalArgumentException("The list of select arguments must not be empty"); + } + QueryContext queryContext = new QueryContext<>(); + queryContext.setSource(sourceList); + queryContext.setResultClass(resultClass); + + // create SelectArgument objects and store in sorted list + List> selectArguments = + IntStream.range(0, arguments.length) + .mapToObj(i -> new SelectArgument<>(i, arguments[i])) + .sorted((a1, a2) -> (Integer.compare(a1.getOrder(), a2.getOrder()))) + .collect(Collectors.toList()); + queryContext.setSelectArguments(Collections.unmodifiableList(selectArguments)); + return queryContext; + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/exceptions/IncorrectQueryException.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/exceptions/IncorrectQueryException.java new file mode 100644 index 0000000..7a7c11c --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/exceptions/IncorrectQueryException.java @@ -0,0 +1,11 @@ +package library.core.exceptions; + +/** + * Checked exception to notify about incorrect query syntax. + */ +public class IncorrectQueryException extends Exception { + + public IncorrectQueryException(String description) { + super(description); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/GroupingCondition.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/GroupingCondition.java new file mode 100644 index 0000000..05c24fe --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/GroupingCondition.java @@ -0,0 +1,45 @@ +package library.core.model; + +import java.util.Map; +import java.util.function.Function; + +/** + * Class wraps and keeps full information about a grouping condition in query's groupBy statement. + * @param source element type + */ +public class GroupingCondition { + /** + * Order of this condition. + */ + private int order; + /** + * Transformation function - labmda or aggregate function. + */ + private Function function; + /** + * Calculated values of this function for each source element (rows). + * These values are used to create groups of elements (rows). + */ + private Map groupedValues; + + public GroupingCondition(int order1, Function function1) { + this.order = order1; + this.function = function1; + } + + public final int getOrder() { + return order; + } + + public final Function getFunction() { + return function; + } + + public final Map getGroupedValues() { + return groupedValues; + } + + public final void setGroupedValues(Map groupedValues1) { + this.groupedValues = groupedValues1; + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/SelectArgument.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/SelectArgument.java new file mode 100644 index 0000000..67d003e --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/SelectArgument.java @@ -0,0 +1,83 @@ +package library.core.model; + +import library.core.model.aggregation.AggregateFunction; + +import java.util.Map; +import java.util.function.Function; + +/** + * Class wraps and keeps full information about a select argument in query. + * @param source element type + */ +public class SelectArgument { + /** + * Order of this argument in query. + */ + private int order; + /** + * Transformation function - lambda or aggregate function. + */ + private Function function; + /** + * Argument value in case of argument is aggregate. + */ + private Object aggregatedValue; + /** + * Argument values mapped to source elements (rows) in case of argument is not aggregate. + */ + private Map values; + /** + * Class type of select argument. + * Used to find result class constructor with required signature. + */ + private Class valueClazz; + + public SelectArgument(int order1, Function function1) { + this.order = order1; + this.function = function1; + } + + public final int getOrder() { + return order; + } + + public final Function getFunction() { + return function; + } + + public final Object getAggregatedValue() { + return aggregatedValue; + } + + public final void setAggregateValue(Object aggregatedValue1) { + this.aggregatedValue = aggregatedValue1; + } + + public final Map getValues() { + return values; + } + + public final void setValues(Map values1) { + this.values = values1; + } + + public final boolean isAggregate() { + return (function instanceof AggregateFunction); + } + + public final AggregateFunction getAggregateFunction() { + if (isAggregate()) { + return (AggregateFunction) function; + } else { + throw new IllegalStateException("This argument is not aggregated"); + } + } + + public final Class getValueClazz() { + return valueClazz; + } + + public final void setValueClazz(Class valueClazz1) { + this.valueClazz = valueClazz1; + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/AggregateFunction.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/AggregateFunction.java new file mode 100644 index 0000000..3fa5f2e --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/AggregateFunction.java @@ -0,0 +1,42 @@ +package library.core.model.aggregation; + +import java.util.Objects; +import java.util.function.Function; + +/** + * Aggregate function. + * @param source element type + * @param result of single function transformation type + * @param aggregated result type + */ +public abstract class AggregateFunction implements Function { + private Function singleFunction; + + public AggregateFunction(Function singleFunction1) { + Objects.requireNonNull(singleFunction1); + this.singleFunction = singleFunction1; + } + + /** + * Because function is aggregate, this applies on many elements, e.g. collection. + * @param elements set of elements to be applied by this aggregate function + * @return aggregation result + */ + public abstract A apply(Iterable elements); + + @Override + public final R apply(S t) { + // delegate single element transformation + return singleFunction.apply(t); + } + + @Override + public final Function compose(Function before) { + throw new UnsupportedOperationException(); + } + + @Override + public final Function andThen(Function after) { + throw new UnsupportedOperationException(); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/impl/AverageFunction.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/impl/AverageFunction.java new file mode 100644 index 0000000..9f8eb99 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/impl/AverageFunction.java @@ -0,0 +1,24 @@ +package library.core.model.aggregation.impl; + +import library.core.model.aggregation.AggregateFunction; +import library.core.utils.NumberUtils; + +import java.util.function.Function; + +public class AverageFunction extends AggregateFunction { + + public AverageFunction(Function singleFunction) { + super(singleFunction); + } + + @Override + public final R apply(Iterable elements) { + long count = 0L; + Number sum = 0; + for (S element : elements) { + count++; + sum = NumberUtils.SUM_NUMBERS.apply(apply(element), sum); + } + return (R) NumberUtils.DIV_NUMBERS.apply(sum, count); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/impl/CountFunction.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/impl/CountFunction.java new file mode 100644 index 0000000..064bf6d --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/impl/CountFunction.java @@ -0,0 +1,21 @@ +package library.core.model.aggregation.impl; + +import library.core.model.aggregation.AggregateFunction; + +import java.util.function.Function; +import java.util.stream.StreamSupport; + +public class CountFunction extends AggregateFunction { + + public CountFunction(Function singleFunction) { + super(singleFunction); + } + + @Override + public final Long apply(Iterable elements) { + // start stream, filter only not null elements and count them + return StreamSupport.stream(elements.spliterator(), false). + filter(e -> e != null). + count(); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/impl/ExtremumFunction.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/impl/ExtremumFunction.java new file mode 100644 index 0000000..75ad113 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/impl/ExtremumFunction.java @@ -0,0 +1,28 @@ +package library.core.model.aggregation.impl; + +import library.core.model.aggregation.AggregateFunction; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public abstract class ExtremumFunction extends AggregateFunction { + + public ExtremumFunction(Function singleFunction) { + super(singleFunction); + } + + @Override + public final R apply(Iterable elements) { + // collect all values (results of transformation function) + List values = new ArrayList<>(); + for (S element : elements) { + R value = this.apply(element); + values.add(value); + } + // search max or min value and return this one + return findExtremum(values); + } + + protected abstract R findExtremum(List values); +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/impl/MaxFunction.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/impl/MaxFunction.java new file mode 100644 index 0000000..e527862 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/impl/MaxFunction.java @@ -0,0 +1,18 @@ +package library.core.model.aggregation.impl; + +import library.core.utils.NumberUtils; + +import java.util.List; +import java.util.function.Function; + +public class MaxFunction extends ExtremumFunction { + + public MaxFunction(Function singleFunction) { + super(singleFunction); + } + + @Override + protected final R findExtremum(List values) { + return values.stream().max(NumberUtils.COMPARE_NUMBERS::apply).get(); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/impl/MinFunction.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/impl/MinFunction.java new file mode 100644 index 0000000..1ee3ec6 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/model/aggregation/impl/MinFunction.java @@ -0,0 +1,18 @@ +package library.core.model.aggregation.impl; + +import library.core.utils.NumberUtils; + +import java.util.List; +import java.util.function.Function; + +public class MinFunction extends ExtremumFunction { + + public MinFunction(Function singleFunction) { + super(singleFunction); + } + + @Override + protected final R findExtremum(List values) { + return values.stream().min(NumberUtils.COMPARE_NUMBERS::apply).get(); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/OperationType.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/OperationType.java new file mode 100644 index 0000000..fceffef --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/OperationType.java @@ -0,0 +1,27 @@ +package library.core.operations; + +public enum OperationType { + WHERE_OP (1), + SIMPLE_SELECT_OP (2), + GROUPING_SELECT_OP (2), + HAVING_OP (3), + ORDER_BY_OP (4), + LIMIT_OP (5), + DISTINCT_OP (6), + UNION_OP (10); + + /** + * Order of operation to be invoked in a query execution sequence. + */ + private int order; + + OperationType(int order1) { + this.order = order1; + } + + public int getOrder() { + return order; + } + + +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/QueryOperation.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/QueryOperation.java new file mode 100644 index 0000000..53f4469 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/QueryOperation.java @@ -0,0 +1,44 @@ +package library.core.operations; + +import library.core.QueryContext; +import library.core.exceptions.IncorrectQueryException; + +import java.util.Comparator; + +/** + * Class represents one query operation, such as "select", "orderBy" and etc. + */ +public interface QueryOperation { + + /** + * Comparator to compare QueryOperation instances by their order. + */ + Comparator ORDER_COMPARATOR = + (o1, o2) -> Integer.compare(o1.getType().getOrder(), o2.getType().getOrder()); + + /** + * @return type of query operation + */ + OperationType getType(); + + /** + * Validates query context before execution this operation. + * By default do nothing. + * @param queryContext execution query context + * @param result type + * @param source elements type + * @throws IncorrectQueryException in case of any incorrect in query found + */ + default void validate(final QueryContext queryContext) throws IncorrectQueryException { + // by default do nothing + } + + /** + * Main method which executes this query operation. + * @param queryContext execution query context + * @param result type + * @param source elements type + * @throws IncorrectQueryException in case of any incorrect in query found + */ + void execute(final QueryContext queryContext) throws IncorrectQueryException; +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/QueryOperationFactory.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/QueryOperationFactory.java new file mode 100644 index 0000000..ed5cd09 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/QueryOperationFactory.java @@ -0,0 +1,32 @@ +package library.core.operations; + +import library.core.operations.impl.*; + +import java.util.HashMap; +import java.util.Map; + +/** + * Factory for QueryOperation instances. + */ +public class QueryOperationFactory { + private static final Map OPERATIONINSTANCES; + static { + OPERATIONINSTANCES = new HashMap<>(); + OPERATIONINSTANCES.put(OperationType.WHERE_OP, new WhereOperation()); + OPERATIONINSTANCES.put(OperationType.SIMPLE_SELECT_OP, new SimpleSelectOperation()); + OPERATIONINSTANCES.put(OperationType.GROUPING_SELECT_OP, new GroupingSelectOperation()); + OPERATIONINSTANCES.put(OperationType.HAVING_OP, new HavingOperation()); + OPERATIONINSTANCES.put(OperationType.ORDER_BY_OP, new OrderByOperation()); + OPERATIONINSTANCES.put(OperationType.LIMIT_OP, new LimitOperation()); + OPERATIONINSTANCES.put(OperationType.DISTINCT_OP, new DistinctOperation()); + OPERATIONINSTANCES.put(OperationType.UNION_OP, new UnionOperation()); + } + + /** + * @param type requested query operation type + * @return query operation instance by requested type + */ + public static QueryOperation getOperationByType(OperationType type) { + return OPERATIONINSTANCES.get(type); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/AbstractSelectOperation.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/AbstractSelectOperation.java new file mode 100644 index 0000000..b74013c --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/AbstractSelectOperation.java @@ -0,0 +1,157 @@ +package library.core.operations.impl; + +import library.core.model.SelectArgument; +import library.core.model.aggregation.AggregateFunction; +import library.core.operations.QueryOperation; +import library.core.QueryContext; +import library.core.exceptions.IncorrectQueryException; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Abstract SelectOperation class which contains main functionality to + * do select statement. + * This class has a common part of SimpleSelect and GroupingSelect + * operations. + */ +public abstract class AbstractSelectOperation implements QueryOperation { + + @Override + public abstract void validate(QueryContext queryContext) throws IncorrectQueryException; + + protected final List doSelect(List sourceElements, QueryContext queryContext) + throws IncorrectQueryException { + List> selectArguments = queryContext.getSelectArguments(); + List> notAggregateArgs = selectArguments.stream(). + filter(a -> !a.isAggregate()). + collect(Collectors.toList()); + List> aggregateArgs = selectArguments.stream(). + filter(a -> a.isAggregate()). + collect(Collectors.toList()); + + // Step#1: calculate value for each select argument + calculateAggregateArguments(sourceElements, aggregateArgs); + calculateNotAggregateArguments(sourceElements, notAggregateArgs); + + // Step#2: create result class instances by using calculated + // in step#1 values of select arguments + Class resultClass = queryContext.getResultClass(); + Class[] constructorArgTypes = getConstructorParameterTypes(selectArguments); + try { + Constructor resultClassConstructor = resultClass.getDeclaredConstructor(constructorArgTypes); + List results = null; + if (!notAggregateArgs.isEmpty() && aggregateArgs.isEmpty()) { + // simple select without aggregations and grouping + // just calculate all select arguments for each source element + results = calculateResult(sourceElements, resultClassConstructor, notAggregateArgs); + } + if (notAggregateArgs.isEmpty() && !aggregateArgs.isEmpty()) { + // exceptional case when all select arguments are aggregate functions + // and no any grouping, so result will be just one row + R result = calculateOnlyAggregatedResult(resultClassConstructor, aggregateArgs); + results = Collections.singletonList(result); + } else if (!notAggregateArgs.isEmpty() && !aggregateArgs.isEmpty()) { + // case when aggregate function in arguments exist + // and not-aggregate also - suppose that they are grouping arguments + // suppose that source elements belong to only one group + // thus we can calculate all results and get first row, because + // the other will be the same + R result = calculateMixedResult(sourceElements.get(0), resultClassConstructor, selectArguments); + results = Collections.singletonList(result); + } + return results; + } catch (NoSuchMethodException e) { + throw new IncorrectQueryException("Constructor of result class = " + resultClass + + " with arg types = " + Arrays.toString(constructorArgTypes) + + " not found"); + } + } + + protected final void calculateNotAggregateArguments(List sourceElements, + List> selectArguments) { + for (SelectArgument selectArgument : selectArguments) { + // apply function for each element of source and store to map [element <-> value] + Map elementValues = new HashMap<>(sourceElements.size()); + for (S element : sourceElements) { + Object value = selectArgument.getFunction().apply(element); + elementValues.put(element, value); + if (selectArgument.getValueClazz() == null) { + selectArgument.setValueClazz(value.getClass()); + } + } + selectArgument.setValues(elementValues); + } + } + + protected final void calculateAggregateArguments(List sourceElements, + List> selectArguments) { + for (SelectArgument selectArgument : selectArguments) { + AggregateFunction aggregateFunction = selectArgument.getAggregateFunction(); + Object aggregateValue = aggregateFunction.apply(sourceElements); + selectArgument.setAggregateValue(aggregateValue); + selectArgument.setValueClazz(aggregateValue.getClass()); + } + } + + protected final List calculateResult(List sourceElements, Constructor resultClassConstructor, + List> selectArguments) throws IncorrectQueryException { + try { + List results = new ArrayList<>(sourceElements.size()); + for (S sourceElement : sourceElements) { + R resultElement = convertToResultObject(resultClassConstructor, sourceElement, selectArguments); + results.add(resultElement); + } + return results; + } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new IncorrectQueryException("Cannot instantiate result class instance: " + e.getMessage()); + } + } + + protected final R calculateOnlyAggregatedResult(Constructor resultClassConstructor, + List> selectArguments) + throws IncorrectQueryException { + try { + return convertToResultObject(resultClassConstructor, null, selectArguments); + } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new IncorrectQueryException("Cannot instantiate result class instance: " + e.getMessage()); + } + } + + protected final R calculateMixedResult(S sourceElement, Constructor resultClassConstructor, + List> selectArguments) throws IncorrectQueryException { + try { + return convertToResultObject(resultClassConstructor, sourceElement, selectArguments); + } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new IncorrectQueryException("Cannot instantiate result class instance: " + e.getMessage()); + } + } + + protected static Class[] getConstructorParameterTypes(List> arguments) { + Class[] types = new Class[arguments.size()]; + // arguments are supposed to be sorted by order + for (int i = 0; i < arguments.size(); i++) { + types[i] = arguments.get(i).getValueClazz(); + } + return types; + } + + protected static R convertToResultObject(Constructor resultConstructor, S source, + List> selectArguments) + throws IllegalAccessException, InvocationTargetException, InstantiationException { + Object[] constructorArguments = new Object[selectArguments.size()]; + for (int i = 0; i < selectArguments.size(); i++) { + SelectArgument selectArgument = selectArguments.get(i); + Object value; + if (selectArgument.isAggregate()) { + value = selectArgument.getAggregatedValue(); + } else { + value = selectArgument.getValues().get(source); + } + constructorArguments[i] = value; + } + return resultConstructor.newInstance(constructorArguments); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/DistinctOperation.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/DistinctOperation.java new file mode 100644 index 0000000..6eefb5a --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/DistinctOperation.java @@ -0,0 +1,32 @@ +package library.core.operations.impl; + +import library.core.operations.OperationType; +import library.core.operations.QueryOperation; +import library.core.QueryContext; + +import java.util.*; + +public final class DistinctOperation implements QueryOperation { + + @Override + public OperationType getType() { + return OperationType.DISTINCT_OP; + } + + @Override + public void execute(QueryContext queryContext) { + // filter duplicates + List result = queryContext.getResult(); + // iterate by list of results and remove duplicated elements + // HashSet used because it can contain the only unique elements + Set distinctElements = new HashSet<>(result.size()); + for (Iterator iterator = result.iterator(); iterator.hasNext();) { + R element = iterator.next(); + if (!distinctElements.contains(element)) { + distinctElements.add(element); + } else { + iterator.remove(); + } + } + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/GroupingSelectOperation.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/GroupingSelectOperation.java new file mode 100644 index 0000000..ff2a064 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/GroupingSelectOperation.java @@ -0,0 +1,121 @@ +package library.core.operations.impl; + +import library.core.exceptions.IncorrectQueryException; +import library.core.model.GroupingCondition; +import library.core.model.SelectArgument; +import library.core.operations.OperationType; +import library.core.QueryContext; + +import java.util.*; + +/** + * This type of operation used when select query is grouping. + * Some additional preparations are required before do select + * for each group of elements. + */ +public final class GroupingSelectOperation extends AbstractSelectOperation { + + @Override + public OperationType getType() { + return OperationType.GROUPING_SELECT_OP; + } + + @Override + public void validate(QueryContext queryContext) throws IncorrectQueryException { + //check QueryContext + List> selectArguments = queryContext.getSelectArguments(); + if (selectArguments == null || selectArguments.isEmpty()) { + throw new IncorrectQueryException("Select statement must have arguments"); + } + for (SelectArgument argument : selectArguments) { + if (argument.getFunction() == null) { + throw new IncorrectQueryException("Select arguments are incorrect"); + } + } + // check that select arguments consist of aggregate functions or grouping conditions only + List> groupingConditions = queryContext.getGroupingConditions(); + long aggregateFunctionsCnt = selectArguments.stream().filter(SelectArgument::isAggregate).count(); + long simpleFunctionsCnt = selectArguments.size() - aggregateFunctionsCnt; + if (simpleFunctionsCnt != groupingConditions.size()) { + throw new IncorrectQueryException("The only grouping expressions or aggregate functions " + + "are permitted in grouping select query."); + } + } + + @Override + public void execute(QueryContext queryContext) throws IncorrectQueryException { + List sourceElements = queryContext.getSource(); + // in case when all rows have been filtered out + if (sourceElements.isEmpty()) { + return; + } + + // Step#1: calculate grouping value for each source element + List> groupingConditions = queryContext.getGroupingConditions(); + calculateGroupingConditionValues(sourceElements, groupingConditions); + + // Step#2: grouping source elements + Map> groupsOfElements = groupElementsByConditions(sourceElements, groupingConditions); + + // Step#3: calculate select argument for each group of elements separately + for (String groupKey : groupsOfElements.keySet()) { + List groupElements = groupsOfElements.get(groupKey); + List groupResults = doSelect(groupElements, queryContext); + queryContext.addResult(groupResults); + } + } + + private Map> groupElementsByConditions(List elements, List> condition) { + // for each source element the groupKey is calculated + Map> groups = new HashMap<>(); + for (S element : elements) { + String groupKey = calculateGroupKeyOfElement(element, condition); + List groupElements = groups.get(groupKey); + if (groupElements == null) { + groupElements = new ArrayList<>(); + groups.put(groupKey, groupElements); + } + groupElements.add(element); + } + return groups; + } + + /* + * GroupKey consists of hashcodes of condition values for this element + * like "84141-14515-6111" in case of 3 grouping conditions and + * hashcode(condition_1_value) = 84141, hashcode(condition_2_value) = 14515, hashcode(condition_3_value) = 6111 + * elements with same groupKey belongs to the same group + * + * @param element source element for which groupKey has to be calculated + * @param conditions list of groupBy conditions + * @param source element type + * @return calculated GroupKey for this element + */ + private static String calculateGroupKeyOfElement(S element, List> conditions) { + StringBuilder groupKeyBuilder = new StringBuilder(); + boolean first = true; + for (GroupingCondition condition : conditions) { + if (!first) { + groupKeyBuilder.append("-"); + } else { + first = false; + } + + Map values = condition.getGroupedValues(); + Object elValue = values.get(element); + groupKeyBuilder.append(elValue.hashCode()); + } + return groupKeyBuilder.toString(); + } + + private void calculateGroupingConditionValues(List elements, List> groupingConditions) { + for (GroupingCondition condition : groupingConditions) { + Map elementValues = new HashMap<>(elements.size()); + for (S element : elements) { + Object groupedValue = condition.getFunction().apply(element); + elementValues.put(element, groupedValue); + } + condition.setGroupedValues(elementValues); + } + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/HavingOperation.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/HavingOperation.java new file mode 100644 index 0000000..e8b799e --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/HavingOperation.java @@ -0,0 +1,28 @@ +package library.core.operations.impl; + +import library.core.operations.OperationType; +import library.core.operations.QueryOperation; +import library.core.QueryContext; + +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class HavingOperation implements QueryOperation { + + @Override + public final OperationType getType() { + return OperationType.HAVING_OP; + } + + @Override + public final void execute(QueryContext queryContext) { + // filter result list by using having predicate + Predicate havingCondition = queryContext.getHaving(); + List filteredResult = queryContext.getResult().stream(). + filter(havingCondition). + collect(Collectors.toList()); + // set filtered result + queryContext.setResult(filteredResult); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/LimitOperation.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/LimitOperation.java new file mode 100644 index 0000000..46167ad --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/LimitOperation.java @@ -0,0 +1,26 @@ +package library.core.operations.impl; + +import library.core.QueryContext; +import library.core.operations.OperationType; +import library.core.operations.QueryOperation; + +import java.util.List; +import java.util.stream.Collectors; + +public class LimitOperation implements QueryOperation { + + @Override + public final OperationType getType() { + return OperationType.LIMIT_OP; + } + + @Override + public final void execute(QueryContext queryContext) { + // limit result list by using stream limit() method + List cutResult = queryContext.getResult().stream(). + limit(queryContext.getLimit()). + collect(Collectors.toList()); + // set cut result list ot context + queryContext.setResult(cutResult); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/OrderByOperation.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/OrderByOperation.java new file mode 100644 index 0000000..cfbf240 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/OrderByOperation.java @@ -0,0 +1,28 @@ +package library.core.operations.impl; + +import library.core.operations.OperationType; +import library.core.operations.QueryOperation; +import library.core.QueryContext; +import org.apache.commons.collections4.comparators.ComparatorChain; + +import java.util.Collections; +import java.util.Comparator; + +public class OrderByOperation implements QueryOperation { + + @Override + public final OperationType getType() { + return OperationType.ORDER_BY_OP; + } + + @Override + public final void execute(QueryContext queryContext) { + // Apache ComparatorChain used to create chain of comparators + // and order by this one + ComparatorChain comparatorChain = new ComparatorChain<>(); + for (Comparator orderByComparator : queryContext.getOrderBy()) { + comparatorChain.addComparator(orderByComparator); + } + Collections.sort(queryContext.getResult(), comparatorChain); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/SimpleSelectOperation.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/SimpleSelectOperation.java new file mode 100644 index 0000000..cb80882 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/SimpleSelectOperation.java @@ -0,0 +1,53 @@ +package library.core.operations.impl; + +import library.core.exceptions.IncorrectQueryException; +import library.core.model.SelectArgument; +import library.core.operations.OperationType; +import library.core.QueryContext; + +import java.util.List; + +/** + * This type of operation used when select query is not grouping. + */ +public class SimpleSelectOperation extends AbstractSelectOperation { + + @Override + public final OperationType getType() { + return OperationType.SIMPLE_SELECT_OP; + } + + @Override + public final void validate(QueryContext queryContext) throws IncorrectQueryException { + //check QueryContext + List> selectArguments = queryContext.getSelectArguments(); + if (selectArguments == null || selectArguments.isEmpty()) { + throw new IncorrectQueryException("Select statement must have arguments"); + } + for (SelectArgument argument : selectArguments) { + if (argument.getFunction() == null) { + throw new IncorrectQueryException("Select arguments are incorrect"); + } + } + // check that there is not aggregate functions at all in "select" statement + // or if exists, then all arguments must be aggregate functions + // because this type of query doesn't have GroupBy condition and + // simple expressions are not permitted in "select" statement + long aggregateArgsCnt = selectArguments.stream().filter(SelectArgument::isAggregate).count(); + if (aggregateArgsCnt != 0 && aggregateArgsCnt != selectArguments.size()) { + throw new IncorrectQueryException("The only grouping expressions or aggregate functions " + + "are permitted in select query with aggregate functions."); + } + } + + @Override + public final void execute(QueryContext queryContext) throws IncorrectQueryException { + List sourceElements = queryContext.getSource(); + // in case when all rows have been filtered out + if (sourceElements.isEmpty()) { + return; + } + List results = doSelect(sourceElements, queryContext); + queryContext.addResult(results); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/UnionOperation.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/UnionOperation.java new file mode 100644 index 0000000..57e9b5f --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/UnionOperation.java @@ -0,0 +1,29 @@ +package library.core.operations.impl; + +import library.api.Query; +import library.core.operations.OperationType; +import library.core.QueryContext; +import library.core.exceptions.IncorrectQueryException; +import library.core.operations.QueryOperation; + +import java.util.LinkedList; +import java.util.List; + +public final class UnionOperation implements QueryOperation { + + @Override + public OperationType getType() { + return OperationType.UNION_OP; + } + + @Override + public void execute(QueryContext queryContext) throws IncorrectQueryException { + for (Query unionQuery : queryContext.getUnions()) { + // execute union query, get its result list and put it into linked list + List unionQueryResult = new LinkedList<>(); + unionQuery.execute().forEach(unionQueryResult::add); + // after that this result is "glued" to our current result + queryContext.addResult(unionQueryResult); + } + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/WhereOperation.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/WhereOperation.java new file mode 100644 index 0000000..4c84413 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/operations/impl/WhereOperation.java @@ -0,0 +1,28 @@ +package library.core.operations.impl; + +import library.core.operations.OperationType; +import library.core.operations.QueryOperation; +import library.core.QueryContext; + +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public final class WhereOperation implements QueryOperation { + + @Override + public OperationType getType() { + return OperationType.WHERE_OP; + } + + @Override + public void execute(QueryContext queryContext) { + Predicate whereCondition = queryContext.getWhere(); + // filter source list by using predicate from "where" statement + List filteredSource = queryContext.getSource().stream(). + filter(whereCondition). + collect(Collectors.toList()); + // set filtered source list to context + queryContext.setSource(filteredSource); + } +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/utils/NumberUtils.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/utils/NumberUtils.java new file mode 100644 index 0000000..8cd1921 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/main/java/library/core/utils/NumberUtils.java @@ -0,0 +1,59 @@ +package library.core.utils; + +import java.util.function.BiFunction; + +/** + * Util class to perform math operations with Number objects. + * + * Important assumption: + * A both arguments are supposed to having the same type, + * e.g. Long and Long, Integer and Integer and so on, + * because the real type is defined by the first argument only. + */ +public class NumberUtils { + + public static final BiFunction SUM_NUMBERS = (n1, n2) -> { + Class numberClazz = n1.getClass(); + if (numberClazz.equals(Integer.class)) { + return n1.intValue() + n2.intValue(); + } else if (numberClazz.equals(Long.class)) { + return n1.longValue() + n2.longValue(); + } else if (numberClazz.equals(Double.class)) { + return n1.doubleValue() + n2.doubleValue(); + } else if (numberClazz.equals(Float.class)) { + return n1.floatValue() + n2.floatValue(); + } else { + throw new IllegalArgumentException("Number class = " + numberClazz + " not supported"); + } + }; + + public static final BiFunction DIV_NUMBERS = (n1, n2) -> { + Class numberClazz = n1.getClass(); + if (numberClazz.equals(Integer.class)) { + return n1.intValue() / n2.intValue(); + } else if (numberClazz.equals(Long.class)) { + return n1.longValue() / n2.longValue(); + } else if (numberClazz.equals(Double.class)) { + return n1.doubleValue() / n2.doubleValue(); + } else if (numberClazz.equals(Float.class)) { + return n1.floatValue() / n2.floatValue(); + } else { + throw new IllegalArgumentException("Number class = " + numberClazz + " not supported"); + } + }; + + public static final BiFunction COMPARE_NUMBERS = (n1, n2) -> { + Class numberClazz = n1.getClass(); + if (numberClazz.equals(Integer.class)) { + return Integer.compare(n1.intValue(), n2.intValue()); + } else if (numberClazz.equals(Long.class)) { + return Long.compare(n1.longValue(), n2.longValue()); + } else if (numberClazz.equals(Double.class)) { + return Double.compare(n1.doubleValue(), n2.doubleValue()); + } else if (numberClazz.equals(Float.class)) { + return Float.compare(n1.floatValue(), n2.floatValue()); + } else { + throw new IllegalArgumentException("Number class = " + numberClazz + " not supported"); + } + }; +} diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/test/java/library/CorrectQueryTest.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/test/java/library/CorrectQueryTest.java new file mode 100644 index 0000000..e865eb9 --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/test/java/library/CorrectQueryTest.java @@ -0,0 +1,283 @@ +package library; + +import client.Statistics; +import client.Student; +import library.api.Conditions; +import library.core.exceptions.IncorrectQueryException; +import org.junit.Test; + +import java.time.LocalDate; +import java.util.Iterator; + +import static client.Student.student; +import static library.api.Aggregates.*; +import static library.api.Conditions.not; +import static library.api.OrderByConditions.asc; +import static library.api.OrderByConditions.desc; +import static library.api.Sources.from; +import static library.api.Sources.list; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Main test class with many test samples. + */ +public final class CorrectQueryTest { + public static final long NUM1 = 1L; + public static final long NUM2 = 2L; + public static final long NUM4 = 4L; + public static final long NUM19 = 19L; + public static final long NUM29 = 29L; + public static final long NUM36 = 36L; + public static final long NUM69 = 69L; + public static final long NUM24 = 24L; + public static final long NUM30 = 30L; + public static final int AGE = 10; + public static final int LIMIT = 100; + + @Test + public void testTaskSampleQuery() throws IncorrectQueryException { + Iterable statistics = + from(list( + student("ivanov", LocalDate.parse("1986-08-06"), "494"), + student("sidorov", LocalDate.parse("1986-08-06"), "495"), + student("smith", LocalDate.parse("1986-08-06"), "495"), + student("petrov", LocalDate.parse("1996-08-06"), "494"))) + .select(Statistics.class, Student::getGroup, count(Student::getGroup), avg(Student::age)) + .where(Conditions.like(Student::getName, ".*ov").and(s -> s.age() > AGE)) + .groupBy(Student::getGroup) + .having(s -> s.getCount() > 0) + .orderBy(asc(Statistics::getGroup), desc(Statistics::getCount)) + .limit(LIMIT) + .union( + from(list(student("ivanov", LocalDate.parse("1985-08-06"), "494"))) + .selectDistinct(Statistics.class, constant("all"), count(s -> 1), + avg(Student::age)) + ) + .execute(); + + Iterator resultIterator = statistics.iterator(); + assertTrue(resultIterator.hasNext()); + assertEquals(new Statistics("all", NUM1, NUM30), resultIterator.next()); + assertTrue(resultIterator.hasNext()); + assertEquals(new Statistics("494", NUM2, NUM24), resultIterator.next()); + assertTrue(resultIterator.hasNext()); + assertEquals(new Statistics("495", NUM1, NUM29), resultIterator.next()); + assertFalse(resultIterator.hasNext()); + } + + @Test + public void testSimpleSelectQuery() throws IncorrectQueryException { + Iterable statistics = + from(list( + student("ivanov", LocalDate.parse("1986-08-06"), "494"), + student("sidorov", LocalDate.parse("1986-08-06"), "495"), + student("smith", LocalDate.parse("1986-08-06"), "495"), + student("petrov", LocalDate.parse("1996-08-06"), "494"))) + .select(Statistics.class, Student::getGroup, s -> NUM1, Student::age) + .orderBy(asc(Statistics::getGroup), asc(Statistics::getAge)) + .execute(); + + Iterator resultIterator = statistics.iterator(); + assertTrue(resultIterator.hasNext()); + assertEquals(new Statistics("494", NUM1, NUM19), resultIterator.next()); + assertTrue(resultIterator.hasNext()); + assertEquals(new Statistics("494", NUM1, NUM29), resultIterator.next()); + assertTrue(resultIterator.hasNext()); + assertEquals(new Statistics("495", NUM1, NUM29), resultIterator.next()); + assertTrue(resultIterator.hasNext()); + assertEquals(new Statistics("495", NUM1, NUM29), resultIterator.next()); + assertFalse(resultIterator.hasNext()); + } + + @Test + public void testSimpleSelectWithWhereQuery() throws IncorrectQueryException { + Iterable statistics = + from(list( + student("ivanov", LocalDate.parse("1986-08-06"), "494"), + student("sidorov", LocalDate.parse("1986-08-06"), "495"), + student("smith", LocalDate.parse("1986-08-06"), "495"), + student("petrov", LocalDate.parse("1996-08-06"), "494"))) + .select(Statistics.class, Student::getGroup, s -> NUM1, Student::age) + .where(not(Conditions.like(Student::getName, ".*ov"))) + .orderBy(asc(Statistics::getGroup), asc(Statistics::getAge)) + .execute(); + + Iterator resultIterator = statistics.iterator(); + assertTrue(resultIterator.hasNext()); + assertEquals(new Statistics("495", NUM1, NUM29), resultIterator.next()); + assertFalse(resultIterator.hasNext()); + } + + @Test + public void testSimpleSelectWithLimitQuery() throws IncorrectQueryException { + Iterable statistics = + from(list( + student("ivanov", LocalDate.parse("1986-08-06"), "494"), + student("sidorov", LocalDate.parse("1986-08-06"), "495"), + student("smith", LocalDate.parse("1986-08-06"), "495"), + student("petrov", LocalDate.parse("1996-08-06"), "494"))) + .select(Statistics.class, Student::getGroup, s -> NUM1, Student::age) + .orderBy(asc(Statistics::getGroup), asc(Statistics::getAge)) + .limit(2) + .execute(); + + Iterator resultIterator = statistics.iterator(); + assertTrue(resultIterator.hasNext()); + assertEquals(new Statistics("494", NUM1, NUM19), resultIterator.next()); + assertTrue(resultIterator.hasNext()); + assertEquals(new Statistics("494", NUM1, NUM29), resultIterator.next()); + assertFalse(resultIterator.hasNext()); + } + + @Test + public void testSelectWithAggregateFunctionsWithoutGroupingQuery() throws IncorrectQueryException { + // find student with min age + Iterable minAge = + from(list( + student("ivanov", LocalDate.parse("1986-08-06"), "494"), + student("sidorov", LocalDate.parse("1986-08-06"), "495"), + student("smith", LocalDate.parse("1986-08-06"), "495"), + student("petrov", LocalDate.parse("1996-08-06"), "494"))) + .select(Statistics.class, constant("min"), constant(1L), min(Student::age)) + .execute(); + + Iterator minAgeIterator = minAge.iterator(); + assertTrue(minAgeIterator.hasNext()); + assertEquals(new Statistics("min", NUM1, NUM19), minAgeIterator.next()); + assertFalse(minAgeIterator.hasNext()); + + // find student with max age + Iterable maxAge = + from(list( + student("ivanov", LocalDate.parse("1986-08-06"), "494"), + student("sidorov", LocalDate.parse("1986-08-06"), "495"), + student("smith", LocalDate.parse("1946-08-06"), "495"), + student("petrov", LocalDate.parse("1996-08-06"), "494"))) + .select(Statistics.class, constant("max"), constant(NUM1), max(Student::age)) + .execute(); + + Iterator maxAgeIterator = maxAge.iterator(); + assertTrue(maxAgeIterator.hasNext()); + assertEquals(new Statistics("max", NUM1, NUM69), maxAgeIterator.next()); + assertFalse(maxAgeIterator.hasNext()); + + // find student with average age of all students + Iterable avgAge = + from(list( + student("ivanov", LocalDate.parse("1986-08-06"), "494"), + student("sidorov", LocalDate.parse("1986-08-06"), "495"), + student("smith", LocalDate.parse("1946-08-06"), "495"), + student("petrov", LocalDate.parse("1996-08-06"), "494"))) + .select(Statistics.class, constant("average"), constant(NUM1), avg(Student::age)) + .execute(); + + Iterator avgAgeIterator = avgAge.iterator(); + assertTrue(avgAgeIterator.hasNext()); + assertEquals(new Statistics("average", NUM1, NUM36), avgAgeIterator.next()); + assertFalse(avgAgeIterator.hasNext()); + + // count all students + Iterable cnt = + from(list( + student("ivanov", LocalDate.parse("1986-08-06"), "494"), + student("sidorov", LocalDate.parse("1986-08-06"), "495"), + student("smith", LocalDate.parse("1946-08-06"), "495"), + student("petrov", LocalDate.parse("1996-08-06"), "494"))) + .select(Statistics.class, constant("all"), count(s -> 1), avg(Student::age)) + .execute(); + + Iterator cntIterator = cnt.iterator(); + assertTrue(cntIterator.hasNext()); + assertEquals(new Statistics("all", NUM4, NUM36), cntIterator.next()); + assertFalse(cntIterator.hasNext()); + } + + @Test + public void testSelectWithAggregateFunctionsWithGroupingQuery() throws IncorrectQueryException { + // find student with min age by groups + Iterable minAge = + from(list( + student("ivanov", LocalDate.parse("1986-08-06"), "494"), + student("sidorov", LocalDate.parse("1986-08-06"), "495"), + student("smith", LocalDate.parse("1986-08-06"), "495"), + student("petrov", LocalDate.parse("1996-08-06"), "494"))) + .select(Statistics.class, Student::getGroup, count(s -> 1), min(Student::age)) + .groupBy(Student::getGroup) + .orderBy(asc(Statistics::getGroup)) + .execute(); + + Iterator minAgeIterator = minAge.iterator(); + assertTrue(minAgeIterator.hasNext()); + assertEquals(new Statistics("494", NUM2, NUM19), minAgeIterator.next()); + assertTrue(minAgeIterator.hasNext()); + assertEquals(new Statistics("495", NUM2, NUM29), minAgeIterator.next()); + assertFalse(minAgeIterator.hasNext()); + } + + @Test + public void testSelectWithManyUnionsQuery() throws IncorrectQueryException { + // find student with min age + Iterable minAndMaxAndAvgAge = + from(list( + student("ivanov", LocalDate.parse("1986-08-06"), "494"), + student("sidorov", LocalDate.parse("1986-08-06"), "495"), + student("smith", LocalDate.parse("1986-08-06"), "495"), + student("petrov", LocalDate.parse("1996-08-06"), "494"))) + .select(Statistics.class, constant("min"), constant(NUM1), min(Student::age)) + .union( + from(list( + student("ivanov", LocalDate.parse("1986-08-06"), "494"), + student("sidorov", LocalDate.parse("1986-08-06"), "495"), + student("smith", LocalDate.parse("1946-08-06"), "495"), + student("petrov", LocalDate.parse("1996-08-06"), "494"))) + .select(Statistics.class, constant("max"), constant(NUM1), max(Student::age)) + ) + .union( + from(list( + student("ivanov", LocalDate.parse("1986-08-06"), "494"), + student("sidorov", LocalDate.parse("1986-08-06"), "495"), + student("smith", LocalDate.parse("1946-08-06"), "495"), + student("petrov", LocalDate.parse("1996-08-06"), "494"))) + .select(Statistics.class, constant("average"), + constant(NUM1), avg(Student::age)) + ) + .execute(); + + Iterator minAndMaxAndAvgAgeIterator = minAndMaxAndAvgAge.iterator(); + assertTrue(minAndMaxAndAvgAgeIterator.hasNext()); + assertEquals(new Statistics("average", NUM1, NUM36), minAndMaxAndAvgAgeIterator.next()); + assertTrue(minAndMaxAndAvgAgeIterator.hasNext()); + assertEquals(new Statistics("max", NUM1, NUM69), minAndMaxAndAvgAgeIterator.next()); + assertTrue(minAndMaxAndAvgAgeIterator.hasNext()); + assertEquals(new Statistics("min", NUM1, NUM19), minAndMaxAndAvgAgeIterator.next()); + assertFalse(minAndMaxAndAvgAgeIterator.hasNext()); + } + + @Test + public void testSelectWithAggregateFunctionsWithManyGroupingConditionsQuery() throws IncorrectQueryException { + // group students by group and age and calculate their count + Iterable minAge = + from(list( + student("ivanov", LocalDate.parse("1986-08-06"), "494"), + student("sidorov", LocalDate.parse("1986-08-06"), "495"), + student("smith", LocalDate.parse("1986-08-06"), "495"), + student("petrov", LocalDate.parse("1996-08-06"), "494"))) + .select(Statistics.class, Student::getGroup, count(s -> 1), Student::age) + .groupBy(Student::getGroup, Student::age) + .orderBy(asc(Statistics::getGroup), asc(Statistics::getAge)) + .execute(); + + Iterator minAgeIterator = minAge.iterator(); + assertTrue(minAgeIterator.hasNext()); + assertEquals(new Statistics("494", NUM1, NUM19), minAgeIterator.next()); + assertTrue(minAgeIterator.hasNext()); + assertEquals(new Statistics("494", NUM1, NUM29), minAgeIterator.next()); + assertTrue(minAgeIterator.hasNext()); + assertEquals(new Statistics("495", NUM2, NUM29), minAgeIterator.next()); + assertFalse(minAgeIterator.hasNext()); + } +} + + diff --git a/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/test/java/library/IncorrectQueryTest.java b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/test/java/library/IncorrectQueryTest.java new file mode 100644 index 0000000..f90ef2c --- /dev/null +++ b/projects/liza22/src/main/java/ru/mipt/diht/students/liza22/collectionql/src/test/java/library/IncorrectQueryTest.java @@ -0,0 +1,111 @@ +package library; + +import client.Statistics; +import client.Student; +import library.api.Conditions; +import library.core.exceptions.IncorrectQueryException; +import org.junit.Test; + +import java.time.LocalDate; + +import static client.Student.student; +import static library.api.Aggregates.avg; +import static library.api.Aggregates.count; +import static library.api.OrderByConditions.asc; +import static library.api.OrderByConditions.desc; +import static library.api.Sources.from; +import static library.api.Sources.list; + +/** + * Test class contains cases with incorrect library usage. + * + * In most such cases library should generate IncorrectQueryException, + * in other cases library generates IllegalArgumentException + * or NullPointerException in case when required argument is null. + */ +public final class IncorrectQueryTest { + public static final int AGE = 10; + public static final int LIMIT = 100; + + @Test(expected = IllegalArgumentException.class) + public void testEmptySourceQuery() throws IncorrectQueryException { + from(list(/* empty */)) + .select(Statistics.class) + .execute(); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyListOfSelectArgs() throws IncorrectQueryException { + from(list(new Object())).select(Object.class).execute(); + } + + @Test(expected = NullPointerException.class) + public void testNullResultClass() throws IncorrectQueryException { + from(list(new Object())).select(null).execute(); + } + + @Test(expected = NullPointerException.class) + public void testNullWhereStatement() throws IncorrectQueryException { + from(list(new Object())).select(Object.class, o -> 1).where(null).execute(); + } + + @Test(expected = NullPointerException.class) + public void testNullOrderByStatement() throws IncorrectQueryException { + from(list(new Object())).select(Object.class, o -> 1).orderBy(null).execute(); + } + + @Test(expected = NullPointerException.class) + public void testNullGroupByStatement() throws IncorrectQueryException { + from(list(new Object())).select(Object.class, o -> 1).groupBy(null).execute(); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyOrderByStatement() throws IncorrectQueryException { + from(list(new Object())).select(Object.class, o -> 1).orderBy().execute(); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyGroupByStatement() throws IncorrectQueryException { + from(list(new Object())).select(Object.class, o -> 1).groupBy().execute(); + } + + @Test(expected = IllegalArgumentException.class) + public void testIncorrectLimitStatement() throws IncorrectQueryException { + from(list(new Object())).select(Object.class, o -> 1).limit(0).execute(); + } + + @Test(expected = IncorrectQueryException.class) + public void testIncorrectSelectArguments() throws IncorrectQueryException { + // class Statistics doesn't have constructor with 2 arguments + Iterable statistics = + from(list( + student("ivanov", LocalDate.parse("1986-08-06"), "494"), + student("sidorov", LocalDate.parse("1986-08-06"), "495"), + student("smith", LocalDate.parse("1986-08-06"), "495"), + student("petrov", LocalDate.parse("1996-08-06"), "494"))) + .select(Statistics.class, Student::getGroup, count(Student::getGroup)) + .where(Conditions.like(Student::getName, ".*ov").and(s -> s.age() > AGE)) + .groupBy(Student::getGroup) + .having(s -> s.getCount() > 0) + .orderBy(asc(Statistics::getGroup), desc(Statistics::getCount)) + .limit(LIMIT) + .execute(); + } + + @Test(expected = IncorrectQueryException.class) + public void testAggregateAndNotAggregateFunctionsInNotGroupingQuery() throws IncorrectQueryException { + // select statement contains Student::getGroup but there is no such grouping function + // in such case the only aggregate functions or constant() are permitted + Iterable statistics = + from(list( + student("ivanov", LocalDate.parse("1986-08-06"), "494"), + student("sidorov", LocalDate.parse("1986-08-06"), "495"), + student("smith", LocalDate.parse("1986-08-06"), "495"), + student("petrov", LocalDate.parse("1996-08-06"), "494"))) + .select(Statistics.class, Student::getGroup, count(Student::getGroup), avg(Student::age)) + .where(Conditions.like(Student::getName, ".*ov").and(s -> s.age() > AGE)) + .orderBy(asc(Statistics::getGroup), desc(Statistics::getCount)) + .limit(LIMIT) + .execute(); + } +} diff --git a/projects/liza22/src/test/java/ru/mipt/diht/students/AppTest.java b/projects/liza22/src/test/java/ru/mipt/diht/students/AppTest.java new file mode 100644 index 0000000..4bb8dd4 --- /dev/null +++ b/projects/liza22/src/test/java/ru/mipt/diht/students/AppTest.java @@ -0,0 +1,38 @@ +package ru.mipt.diht.students; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Unit test for simple App. + */ +public class AppTest + extends TestCase +{ + /** + * Create the test case + * + * @param testName name of the test case + */ + public AppTest( String testName ) + { + super( testName ); + } + + /** + * @return the suite of tests being tested + */ + public static Test suite() + { + return new TestSuite( AppTest.class ); + } + + /** + * Rigourous Test :-) + */ + public void testApp() + { + assertTrue( true ); + } +} diff --git a/projects/pom.xml b/projects/pom.xml index 5d14966..5fc3c22 100644 --- a/projects/pom.xml +++ b/projects/pom.xml @@ -1,5 +1,5 @@ - + + 4.0.0 ru.mipt.diht.students @@ -30,7 +30,8 @@ dkhurtin ale3otik Pitovsky - + liza22 + @@ -133,4 +134,4 @@ - + \ No newline at end of file