Java tutorial
// -*- mode: java; c-basic-offset: 2; -*- // Copyright 2009-2011 Google, All Rights reserved // Copyright 2011-2012 MIT, All rights reserved // Released under the Apache License, Version 2.0 // http://www.apache.org/licenses/LICENSE-2.0 package com.google.appinventor.client.explorer.youngandroid; import static com.google.appinventor.client.Ode.MESSAGES; import java.util.logging.Logger; import java.util.ArrayList; import java.util.List; import com.google.appinventor.client.ErrorReporter; import com.google.appinventor.client.GalleryClient; import com.google.appinventor.client.GalleryGuiFactory; import com.google.appinventor.client.Ode; import com.google.appinventor.client.output.OdeLog; import com.google.appinventor.client.OdeAsyncCallback; import com.google.appinventor.client.boxes.PrivateUserProfileTabPanel; import com.google.appinventor.client.utils.Uploader; import com.google.appinventor.shared.rpc.ServerLayout; import com.google.appinventor.shared.rpc.UploadResponse; import com.google.appinventor.shared.rpc.project.GalleryApp; import com.google.appinventor.shared.rpc.project.GalleryAppListResult; import com.google.appinventor.shared.rpc.project.GalleryComment; import com.google.appinventor.shared.rpc.project.UserProject; import com.google.appinventor.shared.rpc.user.User; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.InputElement; import com.google.gwt.event.dom.client.ChangeEvent; import com.google.gwt.event.dom.client.ChangeHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.ErrorEvent; import com.google.gwt.event.dom.client.ErrorHandler; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Anchor; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FileUpload; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.FocusPanel; import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.Panel; import com.google.gwt.user.client.ui.TabPanel; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.VerticalPanel; /* profileGUI has: profileSingle mainContent -- like app Info userContentTitle userNameLabel userNameBox userLinkLabel userLinkBox userEmailFrequencyBox userEmailFrequencyPrefixLabel userEmailFrequencySuffixLabel appCardWrapper imageUploadBox imageUploadBoxInner userAvatar imageUploadPrompt upload */ /** * The profile page shows a single user's profile information * * It has different modes for public viewing or when user is editing privately * * @author vincentaths@gmail.com (Vincent Zhang) * @author mrburaimo@gmail.com (Daniel Buraimo) */ public class ProfilePage extends Composite/* implements GalleryRequestListener*/ { private List<GalleryApp> apps; private final List<GalleryApp> selectedApps; private static final int ZERO = 0; public static final int PRIVATE = 0; public static final int PUBLIC = 1; public static final int REQUEST_BYDEVELOPER = 7; private int appCatalogCounter = 0; private boolean appCatalogExhausted = false; public static final int NUMAPPSTOSHOW = 10; String userId = "-1"; final int profileStatus; final FileUpload imageUpload = new FileUpload(); // Create GUI wrappers and components private GalleryAppTab appCatalogTab; private final TabPanel appTabs; private final FlowPanel appCatalog; private final FlowPanel appCatalogContent; // The abstract top-level GUI container VerticalPanel profileGUI = new VerticalPanel(); // The actual container that components go in VerticalPanel profileSingle = new VerticalPanel(); // The main profile container, same as appDetails in GalleryPage FlowPanel mainContent = new FlowPanel(); // The sidebar showing a list of apps by this author, same as GalleryPage private TabPanel sidebarTabs = new TabPanel(); // Wrapper for primary profile content (image + userinfo) FlowPanel profilePrimaryWrapper = new FlowPanel(); // Header in this case is basically image-related components FlowPanel profileHeader = new FlowPanel(); FocusPanel profileHeaderWrapper = new FocusPanel(); // Other basic user profile information FlowPanel profileInfo = new FlowPanel(); FocusPanel appCardWrapper = new FocusPanel(); FlowPanel imageUploadBox = new FlowPanel(); FlowPanel imageUploadBoxInner = new FlowPanel(); Image userAvatar = new Image(); Label imageUploadPrompt = new Label(); // the majorContentCard has a label and namebox Label userContentHeader = new Label(); Label usernameLabel = new Label(); Label userLinkLabel = new Label(); Label userEmailDescriptionLabel = new Label(); Label userEmailFrequencyPrefixLabel = new Label(); Label userEmailFrequencySuffixLabel = new Label(); Button editProfile = new Button(MESSAGES.buttonEditProfile()); final TextBox userNameBox = new TextBox(); final TextBox userLinkBox = new TextBox(); final TextBox userEmailFrequencyBox = new TextBox(); final Label userNameDisplay = new Label(); Anchor userLinkDisplay = new Anchor(); final Button profileSubmit = new Button(MESSAGES.buttonUpdateProfile()); private static final Logger LOG = Logger.getLogger(ProfilePage.class.getName()); private static final Ode ode = Ode.getInstance(); GalleryClient gallery = GalleryClient.getInstance(); GalleryGuiFactory galleryGF = new GalleryGuiFactory(); /** * Creates a new ProfilePage, must take in parameters * * @param incomingUserId the string ID of user that we are about to render * @param editStatus the edit status (0 is private, 1 is public) * */ public ProfilePage(final String incomingUserId, final int editStatus) { appCatalog = new FlowPanel(); appCatalogContent = new FlowPanel(); selectedApps = new ArrayList<GalleryApp>(); appTabs = new TabPanel(); // Replace the global variable if (incomingUserId.equalsIgnoreCase("-1")) { // this is user checking out own profile, thus we grab current user info // Get current user id final User currentUser = Ode.getInstance().getUser(); userId = currentUser.getUserId(); } else { // this is checking out an already existing user's profile... userId = incomingUserId; } profileStatus = editStatus; // If we're editing or updating, add input form for image if (editStatus == PRIVATE) { // This should only set up image after userId is returned above } else { // we are just viewing this page so setup the image initReadOnlyImage(); } if (editStatus == PRIVATE) { userContentHeader.setText(MESSAGES.labelEditYourProfile()); usernameLabel.setText(MESSAGES.labelYourDisplayName()); userLinkLabel.setText(MESSAGES.labelMoreInfoLink()); userEmailDescriptionLabel.setText(MESSAGES.labelEmailDescription()); userEmailFrequencyPrefixLabel.setText(MESSAGES.labelEmailFrequencyPrefix()); userEmailFrequencySuffixLabel.setText(MESSAGES.labelEmailFrequencySuffix()); editProfile.setVisible(false); profileSubmit.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { profileSubmit.setEnabled(false); if (!validEmailFrequency(userEmailFrequencyBox.getText())) { Window.alert(MESSAGES.errorEmailFrequency()); profileSubmit.setEnabled(true); return; } // Store the name value of user, modify database final OdeAsyncCallback<Void> userNameUpdateCallback = new OdeAsyncCallback<Void>( // failure message MESSAGES.galleryError()) { @Override public void onSuccess(Void arg0) { profileSubmit.setEnabled(true); } }; ode.getUserInfoService().storeUserName(userNameBox.getText(), userNameUpdateCallback); // Store the link value of user, modify database final OdeAsyncCallback<Void> userLinkUpdateCallback = new OdeAsyncCallback<Void>( // failure message MESSAGES.galleryError()) { @Override public void onSuccess(Void arg0) { } }; if (userLinkBox.getText().isEmpty()) { Ode.getInstance().getUserInfoService().storeUserLink("", userLinkUpdateCallback); } else { Ode.getInstance().getUserInfoService().storeUserLink(userLinkBox.getText(), userLinkUpdateCallback); } // Store the email notification frequency value of user, modofy database final OdeAsyncCallback<Void> userEmailFrequencyUpdateCallback = new OdeAsyncCallback<Void>( // failure message MESSAGES.galleryError()) { @Override public void onSuccess(Void arg0) { } }; Ode.getInstance().getUserInfoService().storeUserEmailFrequency( Integer.valueOf(userEmailFrequencyBox.getText()), userEmailFrequencyUpdateCallback); } }); profileInfo.add(userContentHeader); profileInfo.add(usernameLabel); profileInfo.add(userNameBox); profileInfo.add(userLinkLabel); profileInfo.add(userLinkBox); profileInfo.add(userEmailDescriptionLabel); profileInfo.add(userEmailFrequencyPrefixLabel); profileInfo.add(userEmailFrequencyBox); profileInfo.add(userEmailFrequencySuffixLabel); profileInfo.add(profileSubmit); } else { profileSingle.addStyleName("ode-Public"); // USER PROFILE IN PUBLIC (NON-EDITABLE) STATE // Set up the user info stuff userLinkLabel.setText("More info:"); profileInfo.add(userContentHeader); profileInfo.add(userLinkLabel); profileInfo.add(userLinkDisplay); profileInfo.add(editProfile); } // Add GUI layers in the "main content" container profileHeader.addStyleName("app-header"); //TODO: change a more contextual style name profilePrimaryWrapper.add(profileHeader); // profileImage profileInfo.addStyleName("app-info-container"); profilePrimaryWrapper.add(profileInfo); profilePrimaryWrapper.addStyleName("clearfix"); mainContent.add(profilePrimaryWrapper); // Add styling for user info detail components mainContent.addStyleName("gallery-container"); mainContent.addStyleName("gallery-content-details"); userContentHeader.addStyleName("app-title"); usernameLabel.addStyleName("profile-textlabel"); userNameBox.addStyleName("profile-textbox"); userNameDisplay.addStyleName("profile-textdisplay"); userLinkLabel.addStyleName("profile-textlabel"); userLinkBox.addStyleName("profile-textbox"); userLinkDisplay.addStyleName("profile-textdisplay"); userEmailDescriptionLabel.addStyleName("profile-textlabel-emaildescription"); userEmailFrequencyPrefixLabel.addStyleName("profile-textlabel"); userEmailFrequencySuffixLabel.addStyleName("profile-textlabel"); userEmailFrequencyBox.addStyleName("profile-textbox-small"); editProfile.addStyleName("profile-submit"); profileSubmit.addStyleName("profile-submit"); imageUpload.addStyleName("app-image-upload"); // Add sidebar if (editStatus == PUBLIC) { sidebarTabs.addStyleName("gallery-container"); sidebarTabs.addStyleName("gallery-app-showcase"); } // Setup top level containers // profileSingle is the actual container that components go in profileSingle.addStyleName("gallery-page-single"); // Add containers to the top-tier GUI, initialize profileSingle.add(mainContent); if (editStatus == PUBLIC) { profileSingle.add(appTabs); profileSingle.add(sidebarTabs); } // profileGUI is just the abstract top-level GUI container profileGUI.add(profileSingle); profileGUI.addStyleName("ode-UserProfileWrapper"); profileGUI.addStyleName("gallery"); initWidget(profileGUI); // Retrieve other user info right after GUI is initialized final OdeAsyncCallback<User> userInformationCallback = new OdeAsyncCallback<User>( // failure message MESSAGES.galleryError()) { @Override public void onSuccess(User user) { // Set associate GUI components of public states // In this case it'll return the user of [userId] userContentHeader.setText(user.getUserName()); makeValidLink(userLinkDisplay, user.getUserLink()); userEmailFrequencyBox.setText(String.valueOf(user.getUserEmailFrequency())); } }; if (editStatus == PRIVATE) { User currentUser = Ode.getInstance().getUser(); // In this case it'll return the current user userId = currentUser.getUserId(); userNameBox.setText(currentUser.getUserName()); userLinkBox.setText(currentUser.getUserLink()); userEmailFrequencyBox.setText(String.valueOf(currentUser.getUserEmailFrequency())); } else { // Public state Ode.getInstance().getUserInfoService().getUserInformationByUserId(userId, userInformationCallback); sidebarTabs.setVisible(false); appCatalogTab = new GalleryAppTab(appCatalog, appCatalogContent, userId); appTabs.add(appCatalog, "My Catalog"); appTabs.selectTab(0); appTabs.addStyleName("gallery-app-tabs"); } //TODO this callback should combine with previous ones. Leave it out for now final User user = Ode.getInstance().getUser(); if (incomingUserId.equals(user.getUserId())) { editProfile.setVisible(true); editProfile.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent clickEvent) { ode.switchToPrivateUserProfileView(); PrivateUserProfileTabPanel.getPrivateUserProfileTabPanel().selectTab(0); } }); } else { editProfile.setVisible(false); } } /** * Helper method to validify a hyperlink * @param link the GWT anchor object to validify * @param linktext the actual http link that the anchor should point to */ private void makeValidLink(Anchor link, String linktext) { if (linktext == null) { link.setText("N/A"); } else { if (linktext.isEmpty()) { link.setText("N/A"); } else { linktext = linktext.toLowerCase(); // Validate link format, fill in http part if (!linktext.startsWith("http")) { linktext = "http://" + linktext; } link.setText(linktext); link.setHref(linktext); link.setTarget("_blank"); } } } /** * Helper method called by constructor to initialize image upload components */ private void initImageComponents(String userId) { imageUploadBox.addStyleName("app-image-uploadbox"); imageUploadBox.addStyleName("gallery-editbox"); imageUploadPrompt = new Label("Upload your profile image!"); imageUploadPrompt.addStyleName("gallery-editprompt"); if (gallery.getGallerySettings() != null) { updateUserImage(gallery.getUserImageURL(userId), imageUploadBoxInner); } imageUploadPrompt.addStyleName("app-image-uploadprompt"); //imageUploadBoxInner.add(imageUploadPrompt); // Set the correct handler for servlet side capture imageUpload.setName(ServerLayout.UPLOAD_FILE_FORM_ELEMENT); imageUpload.addChangeHandler(new ChangeHandler() { public void onChange(ChangeEvent event) { uploadImage(); } }); imageUploadBoxInner.add(imageUpload); imageUploadBox.add(imageUploadBoxInner); profileHeaderWrapper.add(imageUploadBox); profileHeaderWrapper.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { // The correct way to trigger click event on FileUpload //imageUpload.getElement().<InputElement>cast().click(); } }); profileHeader.add(profileHeaderWrapper); Label uploadPrompt = new Label("Upload your profile image"); uploadPrompt.addStyleName("primary-link-small"); uploadPrompt.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { // The correct way to trigger click event on FileUpload imageUpload.getElement().<InputElement>cast().click(); } }); profileHeader.add(uploadPrompt); } /** * Helper method called by constructor to create the app image for display */ private void initReadOnlyImage() { updateUserImage(gallery.getUserImageURL(userId), profileHeader); } /** * Main method to validify and upload the app image */ private void uploadImage() { String uploadFilename = imageUpload.getFilename(); if (!uploadFilename.isEmpty()) { String filename = makeValidFilename(uploadFilename); // Forge the request URL for gallery servlet String uploadUrl = GWT.getModuleBaseURL() + ServerLayout.GALLERY_SERVLET + "/user/" + userId + "/" + filename; Uploader.getInstance().upload(imageUpload, uploadUrl, new OdeAsyncCallback<UploadResponse>(MESSAGES.fileUploadError()) { @Override public void onSuccess(UploadResponse uploadResponse) { switch (uploadResponse.getStatus()) { case SUCCESS: ErrorReporter.hide(); imageUploadBoxInner.clear(); updateUserImage(gallery.getUserImageURL(userId), imageUploadBoxInner); break; case FILE_TOO_LARGE: // The user can resolve the problem by uploading a smaller file. ErrorReporter.reportInfo(MESSAGES.fileTooLargeError()); break; default: ErrorReporter.reportError(MESSAGES.fileUploadError()); break; } } }); } } /** * Helper method to validify file name, used in uploadImage() * @param uploadFilename The full filename of the file */ private String makeValidFilename(String uploadFilename) { // Strip leading path off filename. // We need to support both Unix ('/') and Windows ('\\') separators. String filename = uploadFilename .substring(Math.max(uploadFilename.lastIndexOf('/'), uploadFilename.lastIndexOf('\\')) + 1); // We need to strip out whitespace from the filename. filename = filename.replaceAll("\\s", ""); return filename; } /** * Helper method to update the user's image * @param url The URL of the image to show * @param container The container that image widget resides */ private void updateUserImage(final String url, Panel container) { userAvatar = new Image(); //setUrl if the new URL is the same one as it was before; an easy workaround is //to make the URL unique so it forces the browser to reload userAvatar.setUrl(url + "?" + System.currentTimeMillis()); userAvatar.addStyleName("app-image"); if (profileStatus == PRIVATE) { //userAvatar.addStyleName("status-updating"); } // if the user has provided a gallery app image, we'll load it. But if not // the error will occur and we'll load default image userAvatar.addErrorHandler(new ErrorHandler() { public void onError(ErrorEvent event) { userAvatar.setUrl(GalleryApp.DEFAULTUSERIMAGE); } }); container.add(userAvatar); if (gallery.getSystemEnvironment() != null && gallery.getSystemEnvironment().toString().equals("Development")) { final OdeAsyncCallback<String> callback = new OdeAsyncCallback<String>( // failure message MESSAGES.galleryError()) { @Override public void onSuccess(String newUrl) { userAvatar.setUrl(newUrl + "?" + System.currentTimeMillis()); } }; Ode.getInstance().getGalleryService().getBlobServingUrl(url, callback); } } public void loadImage() { initImageComponents(userId); } private boolean validEmailFrequency(String emailFrequencyString) { int emailFrequency = 0; try { emailFrequency = Integer.valueOf(emailFrequencyString); if (emailFrequency < 1) return false; } catch (NumberFormatException e) { return false; } return true; } /** * A wrapper class of tab, which provides help method to get/set UI components */ private class GalleryAppTab { Label buttonNext; Label noResultsFound; Label keywordTotalResultsLabel; Label generalTotalResultsLabel; /** * @param container: the FlowPanel that this app tab will reside. * @param content: the sub-panel that contains the actual app content. * @apram incomingUserId: the user id */ GalleryAppTab(FlowPanel container, FlowPanel content, final String incomingUserId) { addGalleryAppTab(container, content, incomingUserId); } /** * @return Label buttonNext */ public Label getButtonNext() { return buttonNext; } /** * @return Label noResultsFound */ public Label getNoResultsFound() { return noResultsFound; } /** * @return Label keywordTotalResultsLabel */ public Label getKeywordTotalResultsLabel() { return keywordTotalResultsLabel; } /** * Set keywordTotalResultsLabel's text to new text * @param keyword the search keyword * @param num number of results */ public void setKeywordTotalResultsLabel(String keyword, int num) { keywordTotalResultsLabel.setText(MESSAGES.gallerySearchResultsPrefix() + keyword + MESSAGES.gallerySearchResultsInfix() + num + MESSAGES.gallerySearchResultsSuffix()); } /** * @return Label generalTotalResultsLabel */ public Label getGeneralTotalResultsLabel() { return generalTotalResultsLabel; } /** * set generalTotalResultsLabel to new text * @param num number of results */ public void setGeneralTotalResultsLabel(int num) { generalTotalResultsLabel.setText(num + MESSAGES.gallerySearchResultsSuffix()); } /** * Creates the GUI components for a regular app tab. * This method resides here because it needs access to global variables. * @param container: the FlowPanel that this app tab will reside. * @param content: the sub-panel that contains the actual app content. */ private void addGalleryAppTab(FlowPanel container, FlowPanel content, final String incomingUserId) { // Search specific generalTotalResultsLabel = new Label(); container.add(generalTotalResultsLabel); final OdeAsyncCallback<GalleryAppListResult> byAuthorCallback = new OdeAsyncCallback<GalleryAppListResult>( // failure message MESSAGES.galleryError()) { @Override public void onSuccess(GalleryAppListResult appsResult) { refreshApps(appsResult, false); } }; Ode.getInstance().getGalleryService().getDeveloperApps(userId, appCatalogCounter, NUMAPPSTOSHOW, byAuthorCallback); container.add(content); buttonNext = new Label(); buttonNext.setText(MESSAGES.galleryMoreApps()); buttonNext.addStyleName("active"); FlowPanel next = new FlowPanel(); next.add(buttonNext); next.addStyleName("gallery-nav-next"); container.add(next); buttonNext.addClickHandler(new ClickHandler() { // @Override public void onClick(ClickEvent event) { if (!appCatalogExhausted) { // If the next page still has apps to retrieve, do it appCatalogCounter += NUMAPPSTOSHOW; Ode.getInstance().getGalleryService().getDeveloperApps(userId, appCatalogCounter, NUMAPPSTOSHOW, byAuthorCallback); } } }); } } /** * Loads the proper tab GUI with gallery's app data. * @param apps: list of returned gallery apps from callback. */ private void refreshApps(GalleryAppListResult appsResult, boolean refreshable) { appCatalogTab.setGeneralTotalResultsLabel(appsResult.getTotalCount()); if (appsResult.getTotalCount() < NUMAPPSTOSHOW) { // That means there's not enough apps to show (reaches the end) appCatalogExhausted = true; } else { appCatalogExhausted = false; } galleryGF.generateHorizontalAppList(appsResult.getApps(), appCatalogContent, refreshable); if (appsResult.getTotalCount() < NUMAPPSTOSHOW || appCatalogCounter + NUMAPPSTOSHOW >= appsResult.getTotalCount()) { appCatalogTab.getButtonNext().setVisible(false); } } /** * Gets the number of projects * * @return the number of projects */ public int getNumProjects() { return apps.size(); } /** * Gets the number of selected projects * * @return the number of selected projects */ public int getNumSelectedApps() { return selectedApps.size(); } /** * Returns the list of selected projects * * @return the selected projects */ public List<GalleryApp> getSelectedApps() { return selectedApps; } /** * select specific tab index based on given index * @param index */ public void setSelectTabIndex(int index) { appTabs.selectTab(index); } /** * Process the results after retrieving GalleryAppListResult * @param appsResult GalleryAppList Result * @param refreshable whether or not clear container * @see GalleryRequestListener */ public void onAppListRequestCompleted(GalleryAppListResult appsResult, boolean refreshable) { List<GalleryApp> apps = appsResult.getApps(); if (apps != null) refreshApps(appsResult, refreshable); else OdeLog.log("apps was null"); } /** * Process the results after retrieving list of GalleryComment * @see GalleryRequestListener */ public void onCommentsRequestCompleted(List<GalleryComment> comments) { } /** * Process the results after retrieving list of UserProject * @see GalleryRequestListener */ public void onSourceLoadCompleted(UserProject projectInfo) { } }