prepared migrating to notification, added mastodon image upload & download, bug fix

This commit is contained in:
nuclearfog 2022-11-28 22:38:20 +01:00
parent 2eb99730dd
commit b0c9f0efe2
No known key found for this signature in database
GPG Key ID: 03488A185C476379
20 changed files with 228 additions and 75 deletions

View File

@ -3,7 +3,6 @@ package org.nuclearfog.twidda.adapter;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static org.nuclearfog.twidda.backend.utils.StringTools.formatCreationTime;
import android.content.Context;
import android.content.res.Resources;
@ -182,7 +181,7 @@ public class MessageAdapter extends Adapter<ViewHolder> {
holder.username.setText(sender.getUsername());
holder.screenname.setText(sender.getScreenname());
holder.receiver.setText(message.getReceiver().getScreenname());
holder.time.setText(formatCreationTime(resources, message.getTimestamp()));
holder.time.setText(StringTools.formatCreationTime(resources, message.getTimestamp()));
holder.message.setText(text);
if (sender.isVerified()) {
holder.verifiedIcon.setVisibility(VISIBLE);

View File

@ -3,7 +3,6 @@ package org.nuclearfog.twidda.adapter;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static org.nuclearfog.twidda.backend.utils.StringTools.formatCreationTime;
import android.content.Context;
import android.content.res.Resources;
@ -67,6 +66,7 @@ public class UserlistAdapter extends Adapter<ViewHolder> {
private UserLists userlists = new UserLists(0L, 0L);
private int loadingIndex = NO_LOADING;
/**
* @param listener item click listener
*/
@ -153,7 +153,7 @@ public class UserlistAdapter extends Adapter<ViewHolder> {
vh.description.setText(item.getDescription());
vh.username.setText(owner.getUsername());
vh.screenname.setText(owner.getScreenname());
vh.date.setText(formatCreationTime(resources, item.getTimestamp()));
vh.date.setText(StringTools.formatCreationTime(resources, item.getTimestamp()));
vh.member.setText(NUM_FORMAT.format(item.getMemberCount()));
vh.subscriber.setText(NUM_FORMAT.format(item.getSubscriberCount()));
if (settings.imagesEnabled() && !owner.getImageUrl().isEmpty()) {

View File

@ -3,7 +3,7 @@ package org.nuclearfog.twidda.backend.api;
import org.nuclearfog.twidda.backend.lists.Messages;
import org.nuclearfog.twidda.backend.lists.UserLists;
import org.nuclearfog.twidda.backend.lists.Users;
import org.nuclearfog.twidda.backend.update.MediaUpdate;
import org.nuclearfog.twidda.backend.update.MediaStatus;
import org.nuclearfog.twidda.backend.update.ProfileUpdate;
import org.nuclearfog.twidda.backend.update.StatusUpdate;
import org.nuclearfog.twidda.backend.update.UserListUpdate;
@ -515,7 +515,7 @@ public interface Connection {
* @param link link to the image
* @return image bitmap
*/
MediaUpdate downloadImage(String link) throws ConnectionException;
MediaStatus downloadImage(String link) throws ConnectionException;
/**
* updates current user's profile
@ -545,7 +545,7 @@ public interface Connection {
* @param mediaUpdate inputstream with MIME type of the media
* @return media ID
*/
long uploadMedia(MediaUpdate mediaUpdate) throws ConnectionException;
long uploadMedia(MediaStatus mediaUpdate) throws ConnectionException;
/**
* get notification of the current user

View File

@ -21,7 +21,7 @@ import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonUser;
import org.nuclearfog.twidda.backend.lists.Messages;
import org.nuclearfog.twidda.backend.lists.UserLists;
import org.nuclearfog.twidda.backend.lists.Users;
import org.nuclearfog.twidda.backend.update.MediaUpdate;
import org.nuclearfog.twidda.backend.update.MediaStatus;
import org.nuclearfog.twidda.backend.update.ProfileUpdate;
import org.nuclearfog.twidda.backend.update.StatusUpdate;
import org.nuclearfog.twidda.backend.update.UserListUpdate;
@ -44,11 +44,14 @@ import java.util.ArrayList;
import java.util.List;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.BufferedSink;
import okio.Okio;
/**
* Implementation of the Mastodon API
@ -90,8 +93,11 @@ public class Mastodon implements Connection {
private static final String ENDPOINT_LOOKUP_USER = "/api/v1/accounts/lookup";
private static final String ENDPOINT_USERLIST = "/api/v1/lists";
private static final String ENDPOINT_NOTIFICATION = "/api/v1/notifications";
private static final String ENDPOINT_UPLOAD_MEDIA = "/api/v2/media";
private static final String ENDPOINT_MEDIA_STATUS = "/api/v1/media/";
MediaType TYPE_TEXT = MediaType.parse("text/plain");
private static final MediaType TYPE_TEXT = MediaType.parse("text/plain");
private static final MediaType TYPE_STREAM = MediaType.parse("application/octet-stream");
private GlobalSettings settings;
@ -508,6 +514,8 @@ public class Mastodon implements Connection {
public void uploadStatus(StatusUpdate update, long[] mediaIds) throws MastodonException {
List<String> params = new ArrayList<>();
params.add("status=" + StringTools.encode(update.getText()));
// add identifier to prevent duplicate posts
params.add("Idempotency-Key=" + System.currentTimeMillis() / 5000);
params.add("visibility=public");
if (update.getReplyId() > 0)
params.add("in_reply_to_id=" + update.getReplyId());
@ -654,8 +662,23 @@ public class Mastodon implements Connection {
@Override
public MediaUpdate downloadImage(String link) throws MastodonException {
throw new MastodonException("not implemented!"); // todo add implementation
public MediaStatus downloadImage(String link) throws MastodonException {
try {
Request request = new Request.Builder().url(link).get().build();
Response response = client.newCall(request).execute();
ResponseBody body = response.body();
if (response.code() == 200 && body != null) {
MediaType type = body.contentType();
if (type != null) {
String mime = type.toString();
InputStream stream = body.byteStream();
return new MediaStatus(stream, mime);
}
}
throw new MastodonException(response);
} catch (IOException e) {
throw new MastodonException(e);
}
}
@ -678,8 +701,32 @@ public class Mastodon implements Connection {
@Override
public long uploadMedia(MediaUpdate mediaUpdate) throws MastodonException {
throw new MastodonException("not implemented!"); // todo add implementation
public long uploadMedia(MediaStatus mediaUpdate) throws MastodonException {
try {
Response response = post(ENDPOINT_UPLOAD_MEDIA, new ArrayList<>(), mediaUpdate.getStream(), "file");
ResponseBody body = response.body();
if (body != null) {
if (response.code() == 200) {
JSONObject json = new JSONObject(body.string());
return Long.parseLong(json.getString("id"));
}
// wait until processed
else if (response.code() == 202) {
int retryCount = 0;
JSONObject json = new JSONObject(body.string());
long id = Long.parseLong(json.getString("id"));
while (retryCount++ < 10) {
response = get(ENDPOINT_MEDIA_STATUS + id, new ArrayList<>());
if (response.code() == 200)
return id;
Thread.sleep(2000L);
}
}
}
throw new MastodonException(response);
} catch (IOException | JSONException | NumberFormatException | InterruptedException e) {
throw new MastodonException(e);
}
}
@ -951,18 +998,6 @@ public class Mastodon implements Connection {
return get(currentLogin.getHostname(), endpoint, currentLogin.getBearerToken(), params);
}
/**
* create post response with user bearer token
*
* @param endpoint endpoint to use
* @param params additional parameters
* @return POST response
*/
private Response post(String endpoint, List<String> params) throws IOException {
Account currentLogin = settings.getLogin();
return post(currentLogin.getHostname(), endpoint, currentLogin.getBearerToken(), params);
}
/**
* create a GET response
*
@ -979,6 +1014,18 @@ public class Mastodon implements Connection {
return client.newCall(request.build()).execute();
}
/**
* create post response with user bearer token
*
* @param endpoint endpoint to use
* @param params additional parameters
* @return POST response
*/
private Response post(String endpoint, List<String> params) throws IOException {
Account login = settings.getLogin();
return post(login.getHostname(), endpoint, login.getBearerToken(), params);
}
/**
* create a POST response
*
@ -988,7 +1035,43 @@ public class Mastodon implements Connection {
* @return POST response
*/
private Response post(String hostname, String endpoint, @Nullable String bearer, List<String> params) throws IOException {
RequestBody body = RequestBody.create("", TYPE_TEXT);
return post(hostname, endpoint, bearer, params, RequestBody.create("", TYPE_TEXT));
}
/**
* send POST request with file and create response
*
* @param endpoint endpoint url
* @param params additional http parameters
* @return http response
*/
private Response post(String endpoint, List<String> params, InputStream is, String addToKey) throws IOException {
RequestBody data = new RequestBody() {
@Override
public MediaType contentType() {
return TYPE_STREAM;
}
@Override
public void writeTo(@NonNull BufferedSink sink) throws IOException {
sink.writeAll(Okio.buffer(Okio.source(is)));
}
};
Account login = settings.getLogin();
RequestBody body = new MultipartBody.Builder().setType(MultipartBody.FORM).addFormDataPart(addToKey, StringTools.getRandomString(), data).build();
return post(login.getHostname(), endpoint, login.getBearerToken(), params, body);
}
/**
* send POST request
*
* @param hostname hostname of a Mastodon instance
* @param endpoint POST endpoint to use
* @param bearer bearer token to authenticate
* @param params additional parameters
* @return POST response
*/
private Response post(String hostname, String endpoint, @Nullable String bearer, List<String> params, RequestBody body) throws IOException {
Request.Builder request = new Request.Builder().url(buildUrl(hostname, endpoint, params)).post(body);
if (bearer != null) {
request.addHeader("Authorization", "Bearer " + bearer);

View File

@ -44,6 +44,10 @@ public class MastodonException extends ConnectionException {
errorCode = RATE_LIMIT_EX;
break;
case 422:
errorCode = INVALID_MEDIA;
break;
case 503:
errorCode = SERVICE_UNAVAILABLE;
break;

View File

@ -34,7 +34,7 @@ public class MastodonNotification implements Notification {
String typeStr = json.getString("type");
JSONObject statusJson = json.optJSONObject("status");
JSONObject userJson = json.getJSONObject("account");
createdAt = StringTools.getTime2(json.getString("created_at"));
createdAt = StringTools.getTime(json.getString("created_at"), StringTools.TIME_MASTODON);
user = new MastodonUser(userJson);
switch (typeStr) {

View File

@ -46,7 +46,7 @@ public class MastodonStatus implements Status {
String replyUserIdStr = json.optString("in_reply_to_account_id", "0");
author = new MastodonUser(json.getJSONObject("account"), currentUserId);
createdAt = StringTools.getTime2(json.optString("created_at"));
createdAt = StringTools.getTime(json.optString("created_at"), StringTools.TIME_MASTODON);
replyCount = json.optInt("replies_count");
reblogCount = json.optInt("reblogs_count");
favoriteCount = json.optInt("favourites_count");

View File

@ -44,7 +44,7 @@ public class MastodonUser implements User {
String idStr = json.getString("id");
screenname = json.optString("acct", "");
username = json.optString("display_name");
createdAt = StringTools.getTime2(json.optString("created_at", ""));
createdAt = StringTools.getTime(json.optString("created_at", ""), StringTools.TIME_MASTODON);
profileUrl = json.optString("avatar");
bannerUrl = json.optString("banner");
description = json.optString("note");

View File

@ -19,13 +19,14 @@ import org.nuclearfog.twidda.backend.api.twitter.impl.RelationV1;
import org.nuclearfog.twidda.backend.api.twitter.impl.TrendV1;
import org.nuclearfog.twidda.backend.api.twitter.impl.TweetV1;
import org.nuclearfog.twidda.backend.api.twitter.impl.TwitterAccount;
import org.nuclearfog.twidda.backend.api.twitter.impl.TwitterNotification;
import org.nuclearfog.twidda.backend.api.twitter.impl.UserListV1;
import org.nuclearfog.twidda.backend.api.twitter.impl.UserV1;
import org.nuclearfog.twidda.backend.api.twitter.impl.UserV2;
import org.nuclearfog.twidda.backend.lists.Messages;
import org.nuclearfog.twidda.backend.lists.UserLists;
import org.nuclearfog.twidda.backend.lists.Users;
import org.nuclearfog.twidda.backend.update.MediaUpdate;
import org.nuclearfog.twidda.backend.update.MediaStatus;
import org.nuclearfog.twidda.backend.update.ProfileUpdate;
import org.nuclearfog.twidda.backend.update.StatusUpdate;
import org.nuclearfog.twidda.backend.update.UserListUpdate;
@ -972,7 +973,7 @@ public class Twitter implements Connection {
@Override
public long uploadMedia(MediaUpdate mediaUpdate) throws TwitterException {
public long uploadMedia(MediaStatus mediaUpdate) throws TwitterException {
List<String> params = new ArrayList<>();
String state;
boolean enableChunk;
@ -1056,7 +1057,7 @@ public class Twitter implements Connection {
@Override
public MediaUpdate downloadImage(String link) throws TwitterException {
public MediaStatus downloadImage(String link) throws TwitterException {
try {
// this type of link requires authentication
if (link.startsWith(DOWNLOAD)) {
@ -1067,7 +1068,7 @@ public class Twitter implements Connection {
if (type != null) {
String mime = type.toString();
InputStream stream = body.byteStream();
return new MediaUpdate(stream, mime);
return new MediaStatus(stream, mime);
}
}
throw new TwitterException(response);
@ -1082,7 +1083,7 @@ public class Twitter implements Connection {
if (type != null) {
String mime = type.toString();
InputStream stream = body.byteStream();
return new MediaUpdate(stream, mime);
return new MediaStatus(stream, mime);
}
}
throw new TwitterException(response);
@ -1145,7 +1146,12 @@ public class Twitter implements Connection {
@Override
public List<Notification> getNotifications(long minId, long maxId) throws ConnectionException {
throw new TwitterException("not supported!");
List<Status> mentions = getMentionTimeline(minId, maxId);
List<Notification> result = new ArrayList<>(mentions.size());
for (Status status : mentions) {
result.add(new TwitterNotification(status));
}
return result;
}
/**
@ -1595,6 +1601,7 @@ public class Twitter implements Connection {
* @param endpoint endpoint url
* @param params additional http parameters
* @param enableChunk true to enable file chunk
* @param addToKey key to add the file
* @return http response
*/
private Response post(String endpoint, List<String> params, InputStream is, String addToKey, boolean enableChunk) throws IOException {

View File

@ -96,7 +96,7 @@ public class TweetV1 implements Status {
isFavorited = json.optBoolean("favorited");
isRetweeted = json.optBoolean("retweeted");
isSensitive = json.optBoolean("possibly_sensitive");
timestamp = StringTools.getTime1(json.optString("created_at", ""));
timestamp = StringTools.getTime(json.optString("created_at", ""), StringTools.TIME_TWITTER_V1);
coordinates = getLocation(json);
mediaLinks = addMedia(json);
userMentions = StringTools.getUserMentions(text, author.getScreenname());

View File

@ -0,0 +1,53 @@
package org.nuclearfog.twidda.backend.api.twitter.impl;
import org.nuclearfog.twidda.model.Notification;
import org.nuclearfog.twidda.model.Status;
import org.nuclearfog.twidda.model.User;
/**
* Twitter implementation of a notification
* Twitter currently only supports mentions as notification
*
* @author nuclearfog
*/
public class TwitterNotification implements Notification {
private static final long serialVersionUID = -2434138376220697796L;
private Status status;
public TwitterNotification(Status status) {
this.status = status;
}
@Override
public long getId() {
return status.getId();
}
@Override
public int getType() {
return TYPE_MENTION;
}
@Override
public long createdAt() {
return status.getTimestamp();
}
@Override
public User getUser() {
return status.getAuthor();
}
@Override
public Status getStatus() {
return status;
}
}

View File

@ -42,7 +42,7 @@ public class UserListV1 implements UserList {
String idStr = json.getString("id_str");
owner = new UserV1(json.getJSONObject("user"), currentId);
createdAt = StringTools.getTime1(json.optString("created_at", ""));
createdAt = StringTools.getTime(json.optString("created_at", ""), StringTools.TIME_TWITTER_V1);
title = json.optString("name", "");
description = json.optString("description", "");
memberCount = json.optInt("member_count");

View File

@ -66,7 +66,7 @@ public class UserV1 implements User {
favoriteCount = json.optInt("favourites_count");
followReqSent = json.optBoolean("follow_request_sent");
defaultImage = json.optBoolean("default_profile_image");
created = StringTools.getTime1(json.optString("created_at", ""));
created = StringTools.getTime(json.optString("created_at", ""), StringTools.TIME_TWITTER_V1);
description = getDescription(json);
url = getUrl(json);

View File

@ -56,7 +56,7 @@ public class UserV2 implements User {
location = json.optString("location", "");
isVerified = json.optBoolean("verified");
profileBannerUrl = json.optString("profile_banner_url", "");
created = StringTools.getTime2(json.optString("created_at", ""));
created = StringTools.getTime(json.optString("created_at", ""), StringTools.TIME_TWITTER_V2);
defaultImage = profileImageUrl.contains("default_profile_images");
url = getUrl(json);

View File

@ -8,7 +8,7 @@ import androidx.annotation.Nullable;
import org.nuclearfog.twidda.backend.api.Connection;
import org.nuclearfog.twidda.backend.api.ConnectionException;
import org.nuclearfog.twidda.backend.api.ConnectionManager;
import org.nuclearfog.twidda.backend.update.MediaUpdate;
import org.nuclearfog.twidda.backend.update.MediaStatus;
import org.nuclearfog.twidda.backend.utils.StringTools;
import org.nuclearfog.twidda.ui.activities.ImageViewer;
@ -52,7 +52,7 @@ public class ImageLoader extends AsyncTask<Uri, Uri, Boolean> {
// download imaged to a local cache folder
for (Uri link : links) {
// get input stream
MediaUpdate mediaUpdate = connection.downloadImage(link.toString());
MediaStatus mediaUpdate = connection.downloadImage(link.toString());
InputStream input = mediaUpdate.getStream();
String mimeType = mediaUpdate.getMimeType();

View File

@ -5,7 +5,7 @@ import android.os.AsyncTask;
import org.nuclearfog.twidda.backend.api.Connection;
import org.nuclearfog.twidda.backend.api.ConnectionException;
import org.nuclearfog.twidda.backend.api.ConnectionManager;
import org.nuclearfog.twidda.backend.update.MediaUpdate;
import org.nuclearfog.twidda.backend.update.MediaStatus;
import org.nuclearfog.twidda.backend.update.StatusUpdate;
import org.nuclearfog.twidda.ui.activities.StatusEditor;
@ -40,7 +40,7 @@ public class StatusUpdater extends AsyncTask<StatusUpdate, Void, Void> {
StatusUpdate statusUpdate = statusUpdates[0];
try {
// upload media first
MediaUpdate[] mediaUpdates = statusUpdate.getMediaUpdates();
MediaStatus[] mediaUpdates = statusUpdate.getMediaUpdates();
long[] mediaIds = new long[mediaUpdates.length];
for (int pos = 0; pos < mediaUpdates.length; pos++) {
// upload media file and save media ID

View File

@ -10,7 +10,7 @@ import java.io.InputStream;
*
* @author nuclearfog
*/
public class MediaUpdate {
public class MediaStatus {
private InputStream inputStream;
private String mimeType;
@ -19,7 +19,7 @@ public class MediaUpdate {
* @param inputStream stream of the media (local or online)
* @param mimeType MIME type e.g. image/jpeg
*/
public MediaUpdate(InputStream inputStream, String mimeType) {
public MediaStatus(InputStream inputStream, String mimeType) {
this.inputStream = inputStream;
this.mimeType = mimeType;
}

View File

@ -19,7 +19,7 @@ import java.io.InputStream;
public class MessageUpdate {
private Uri uri;
private MediaUpdate mediaUpdate;
private MediaStatus mediaUpdate;
private String name = "";
private String text = "";
@ -62,7 +62,7 @@ public class MessageUpdate {
* @return input stream
*/
@Nullable
public MediaUpdate getMediaUpdate() {
public MediaStatus getMediaUpdate() {
return mediaUpdate;
}
@ -103,7 +103,7 @@ public class MessageUpdate {
String mimeType = resolver.getType(uri);
InputStream fileStream = resolver.openInputStream(uri);
if (fileStream != null && mimeType != null && fileStream.available() > 0) {
mediaUpdate = new MediaUpdate(fileStream, mimeType);
mediaUpdate = new MediaStatus(fileStream, mimeType);
return true;
}
} catch (IOException e) {

View File

@ -25,7 +25,7 @@ public class StatusUpdate {
private double latitude;
private List<Uri> mediaUris = new ArrayList<>(5);
private MediaUpdate[] mediaUpdates = {};
private MediaStatus[] mediaUpdates = {};
private boolean hasLocation = false;
/**
@ -93,7 +93,7 @@ public class StatusUpdate {
*
* @return list of media updates
*/
public MediaUpdate[] getMediaUpdates() {
public MediaStatus[] getMediaUpdates() {
return mediaUpdates;
}
@ -152,13 +152,13 @@ public class StatusUpdate {
return true;
try {
// open input streams
mediaUpdates = new MediaUpdate[mediaUris.size()];
mediaUpdates = new MediaStatus[mediaUris.size()];
for (int i = 0; i < mediaUpdates.length; i++) {
InputStream is = resolver.openInputStream(mediaUris.get(i));
String mime = resolver.getType(mediaUris.get(i));
// check if stream is valid
if (is != null && mime != null && is.available() > 0) {
mediaUpdates[i] = new MediaUpdate(is, mime);
mediaUpdates[i] = new MediaStatus(is, mime);
} else {
return false;
}
@ -174,7 +174,7 @@ public class StatusUpdate {
* close all open streams
*/
public void close() {
for (MediaUpdate mediaUpdate : mediaUpdates) {
for (MediaStatus mediaUpdate : mediaUpdates) {
mediaUpdate.close();
}
}

View File

@ -14,6 +14,7 @@ import java.util.Date;
import java.util.Locale;
import java.util.Random;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -51,11 +52,17 @@ public final class StringTools {
*/
private static final SimpleDateFormat dateFormat2 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
private static final TimeZone TIME_ZONE = TimeZone.getDefault();
/**
* fallback date if parsing failed
*/
private static final long DEFAULT_TIME = 0x61D99F64;
public static final int TIME_TWITTER_V1 = 0xE16A;
public static final int TIME_TWITTER_V2 = 0x3F5C;
public static final int TIME_MASTODON = 0x5105;
/**
* random generator used to generate random strings
*/
@ -184,33 +191,33 @@ public final class StringTools {
}
/**
* convert Twitter API 1.1 date time to long format
* convert time strings from different APIs to the local format
*
* @param timeStr Twitter time string
* @param timeStr Twitter time string
* @param timeFormat API format to use {@link #TIME_TWITTER_V1,#TIME_TWITTER_V2,#TIME_MASTODON}
* @return date time
*/
public static long getTime1(String timeStr) {
public static long getTime(String timeStr, int timeFormat) {
try {
Date date = dateFormat1.parse(timeStr);
if (date != null)
return date.getTime();
} catch (ParseException e) {
e.printStackTrace();
}
return DEFAULT_TIME;
}
switch (timeFormat) {
case TIME_TWITTER_V1:
Date result = dateFormat1.parse(timeStr);
if (result != null)
return result.getTime();
break;
/**
* convert Twitter API 2 date time to long format
*
* @param timeStr Twitter time string
* @return date time
*/
public static long getTime2(String timeStr) {
try {
Date date = dateFormat2.parse(timeStr);
if (date != null)
return date.getTime();
case TIME_TWITTER_V2:
result = dateFormat2.parse(timeStr);
if (result != null)
return result.getTime();
break;
case TIME_MASTODON:
result = dateFormat2.parse(timeStr);
if (result != null) // temporary fix: Mastodon time depends on timezone
return result.getTime() + TIME_ZONE.getOffset(new Date().getTime());
break;
}
} catch (ParseException e) {
e.printStackTrace();
}