/projects/rssowl-2.0.5/org.rssowl.ui/src/org/rssowl/ui/internal/editors/feed/NewsContentProvider.java
Java | 819 lines | 516 code | 150 blank | 153 comment | 203 complexity | a24e74a801919d723147209337d322e0 MD5 | raw file
- /* ********************************************************************** **
- ** Copyright notice **
- ** **
- ** (c) 2005-2009 RSSOwl Development Team **
- ** http://www.rssowl.org/ **
- ** **
- ** All rights reserved **
- ** **
- ** This program and the accompanying materials are made available under **
- ** the terms of the Eclipse Public License v1.0 which accompanies this **
- ** distribution, and is available at: **
- ** http://www.rssowl.org/legal/epl-v10.html **
- ** **
- ** A copy is found in the file epl-v10.html and important notices to the **
- ** license from the team is found in the textfile LICENSE.txt distributed **
- ** in this package. **
- ** **
- ** This copyright notice MUST APPEAR in all copies of the file! **
- ** **
- ** Contributors: **
- ** RSSOwl Development Team - initial API and implementation **
- ** **
- ** ********************************************************************** */
- package org.rssowl.ui.internal.editors.feed;
- import org.eclipse.core.runtime.IProgressMonitor;
- import org.eclipse.jface.viewers.ITreeContentProvider;
- import org.eclipse.jface.viewers.Viewer;
- import org.eclipse.swt.widgets.Tree;
- import org.eclipse.swt.widgets.TreeItem;
- import org.rssowl.core.persist.IBookMark;
- import org.rssowl.core.persist.IEntity;
- import org.rssowl.core.persist.IFolder;
- import org.rssowl.core.persist.IMark;
- import org.rssowl.core.persist.INews;
- import org.rssowl.core.persist.INewsBin;
- import org.rssowl.core.persist.INewsMark;
- import org.rssowl.core.persist.ISearchMark;
- import org.rssowl.core.persist.dao.DynamicDAO;
- import org.rssowl.core.persist.event.NewsAdapter;
- import org.rssowl.core.persist.event.NewsEvent;
- import org.rssowl.core.persist.event.NewsListener;
- import org.rssowl.core.persist.event.SearchMarkAdapter;
- import org.rssowl.core.persist.event.SearchMarkEvent;
- import org.rssowl.core.persist.event.runnable.EventType;
- import org.rssowl.core.persist.reference.BookMarkReference;
- import org.rssowl.core.persist.reference.FeedLinkReference;
- import org.rssowl.core.persist.reference.ModelReference;
- import org.rssowl.core.persist.reference.NewsBinReference;
- import org.rssowl.core.persist.reference.NewsReference;
- import org.rssowl.core.persist.reference.SearchMarkReference;
- import org.rssowl.core.persist.service.PersistenceException;
- import org.rssowl.ui.internal.Controller;
- import org.rssowl.ui.internal.EntityGroup;
- import org.rssowl.ui.internal.EntityGroupItem;
- import org.rssowl.ui.internal.FolderNewsMark;
- import org.rssowl.ui.internal.FolderNewsMark.FolderNewsMarkReference;
- import org.rssowl.ui.internal.util.JobRunner;
- import org.rssowl.ui.internal.util.UIBackgroundJob;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.HashSet;
- import java.util.List;
- import java.util.Set;
- /**
- * @author bpasero
- */
- public class NewsContentProvider implements ITreeContentProvider {
- private final NewsBrowserViewer fBrowserViewer;
- private final NewsTableViewer fTableViewer;
- private final NewsGrouping fGrouping;
- private final NewsFilter fFilter;
- private NewsListener fNewsListener;
- private SearchMarkAdapter fSearchMarkListener;
- private INewsMark fInput;
- private final FeedView fFeedView;
- private boolean fDisposed;
- /* Cache displayed News */
- private Set<INews> fCachedNews;
- /**
- * @param tableViewer
- * @param browserViewer
- * @param feedView
- */
- public NewsContentProvider(NewsTableViewer tableViewer, NewsBrowserViewer browserViewer, FeedView feedView) {
- fTableViewer = tableViewer;
- fBrowserViewer = browserViewer;
- fFeedView = feedView;
- fGrouping = feedView.getGrouper();
- fFilter = feedView.getFilter();
- }
- /*
- * @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object)
- */
- public Object[] getElements(Object inputElement) {
- List<Object> elements = new ArrayList<Object>();
- /* Wrap into Object Array */
- if (!(inputElement instanceof Object[]))
- inputElement = new Object[] { inputElement };
- /* Foreach Object */
- Object[] objects = (Object[]) inputElement;
- for (Object object : objects) {
- /* This is a News */
- if (object instanceof INews && ((INews) object).isVisible()) {
- elements.add(object);
- }
- /* This is a NewsReference */
- else if (object instanceof NewsReference) {
- NewsReference newsRef = (NewsReference) object;
- INews news = obtainFromCache(newsRef);
- if (news != null)
- elements.add(news);
- }
- /* This is a FeedReference */
- else if (object instanceof FeedLinkReference) {
- synchronized (NewsContentProvider.this) {
- Collection<INews> news = fCachedNews;
- if (news != null) {
- if (fGrouping.getType() == NewsGrouping.Type.NO_GROUPING)
- elements.addAll(news);
- else
- elements.addAll(fGrouping.group(news));
- }
- }
- }
- /* This is a class that implements IMark */
- else if (object instanceof ModelReference) {
- Class<? extends IEntity> entityClass = ((ModelReference) object).getEntityClass();
- if (IMark.class.isAssignableFrom(entityClass) || IFolder.class.isAssignableFrom(entityClass)) { //Suppoer FolderNewsMark too
- synchronized (NewsContentProvider.this) {
- Collection<INews> news = fCachedNews;
- if (news != null) {
- if (fGrouping.getType() == NewsGrouping.Type.NO_GROUPING)
- elements.addAll(news);
- else
- elements.addAll(fGrouping.group(news));
- }
- }
- }
- }
- /* This is a EntityGroup */
- else if (object instanceof EntityGroup) {
- EntityGroup group = (EntityGroup) object;
- List<EntityGroupItem> items = group.getItems();
- for (EntityGroupItem item : items) {
- if (((INews) item.getEntity()).isVisible())
- elements.add(item.getEntity());
- }
- }
- }
- return elements.toArray();
- }
- /*
- * @see org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.Object)
- */
- public Object[] getChildren(Object parentElement) {
- List<Object> children = new ArrayList<Object>();
- /* Handle EntityGroup */
- if (parentElement instanceof EntityGroup) {
- List<EntityGroupItem> items = ((EntityGroup) parentElement).getItems();
- for (EntityGroupItem item : items)
- children.add(item.getEntity());
- }
- return children.toArray();
- }
- /*
- * @see org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object)
- */
- public Object getParent(Object element) {
- /* Handle Grouping specially */
- if (fGrouping.isActive() && element instanceof INews) {
- Collection<EntityGroup> groups = fGrouping.group(Collections.singletonList((INews) element));
- if (groups.size() == 1)
- return groups.iterator().next();
- }
- return null;
- }
- /*
- * @see org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.Object)
- */
- public boolean hasChildren(Object element) {
- return element instanceof EntityGroup;
- }
- /*
- * @see org.eclipse.jface.viewers.IContentProvider#dispose()
- */
- public synchronized void dispose() {
- fDisposed = true;
- unregisterListeners();
- }
- /*
- * @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer,
- * java.lang.Object, java.lang.Object)
- */
- public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
- /* Ignore - Input changes are handled via refreshCache(Object input) */
- }
- boolean isGroupingEnabled() {
- return fGrouping.getType() != NewsGrouping.Type.NO_GROUPING;
- }
- boolean isGroupingByFeed() {
- return fGrouping.getType() == NewsGrouping.Type.GROUP_BY_FEED;
- }
- boolean isGroupingByStickyness() {
- return fGrouping.getType() == NewsGrouping.Type.GROUP_BY_STICKY;
- }
- /* Returns the news that have been added since the last refresh */
- synchronized List<INews> refreshCache(INewsMark input, boolean onlyAdd) throws PersistenceException {
- List<INews> addedNews = Collections.emptyList();
- /* Update Input */
- fInput = input;
- /* Register Listeners if not yet done */
- if (fNewsListener == null)
- registerListeners();
- /* Clear old Data if required */
- if (fCachedNews == null)
- fCachedNews = new HashSet<INews>();
- else if (!onlyAdd)
- fCachedNews.clear();
- /* Check if ContentProvider was already disposed */
- if (fDisposed)
- return addedNews;
- /* Obtain the News */
- addedNews = new ArrayList<INews>();
- /* Special-case marks that can retrieve newsRefs cheaply */
- if (input.isGetNewsRefsEfficient()) {
- for (NewsReference newsRef : input.getNewsRefs(INews.State.getVisible())) {
- /* Avoid to resolve an already shown News */
- if (onlyAdd && hasCachedNews(newsRef))
- continue;
- /* Resolve and Add News */
- INews resolvedNews = newsRef.resolve();
- if (resolvedNews != null)
- addedNews.add(resolvedNews);
- }
- }
- else
- addedNews.addAll(input.getNews(INews.State.getVisible()));
- /* Add into Cache */
- fCachedNews.addAll(addedNews);
- return addedNews;
- }
- synchronized INewsMark getInput() {
- return fInput;
- }
- synchronized Collection<INews> getCachedNewsCopy() {
- if (fCachedNews == null)
- return null;
- return new ArrayList<INews>(fCachedNews);
- }
- synchronized boolean hasCachedNews() {
- return fCachedNews != null && !fCachedNews.isEmpty();
- }
- private synchronized boolean hasCachedNews(NewsReference ref) {
- if (fCachedNews == null)
- return false;
- for (INews news : fCachedNews) {
- if (ref.references(news))
- return true;
- }
- return false;
- }
- private synchronized boolean hasCachedNews(INews news) {
- if (fCachedNews == null)
- return false;
- for (INews cachedNews : fCachedNews) {
- if (cachedNews.equals(news))
- return true;
- }
- return false;
- }
- private synchronized INews obtainFromCache(NewsReference ref) {
- for (INews cachedNews : fCachedNews) {
- if (ref.references(cachedNews))
- return cachedNews;
- }
- return null;
- }
- private void registerListeners() {
- /* Saved Search Listener */
- fSearchMarkListener = new SearchMarkAdapter() {
- @Override
- public void resultsChanged(final Set<SearchMarkEvent> events) {
- for (SearchMarkEvent event : events) {
- ISearchMark searchMark = event.getEntity();
- if (fInput.equals(searchMark)) {
- JobRunner.runInUIThread(fFeedView.getEditorControl(), new Runnable() {
- public void run() {
- final boolean onlyHandleAddedNews = fFeedView.isVisible();
- JobRunner.runUIUpdater(new UIBackgroundJob(fFeedView.getEditorControl()) {
- private List<INews> fAddedNews;
- @Override
- protected void runInBackground(IProgressMonitor monitor) {
- if (!Controller.getDefault().isShuttingDown())
- fAddedNews = refreshCache(fInput, onlyHandleAddedNews);
- }
- @Override
- protected void runInUI(IProgressMonitor monitor) {
- if (Controller.getDefault().isShuttingDown())
- return;
- /* Check if we need to Refresh at all */
- if (onlyHandleAddedNews && (fAddedNews == null || fAddedNews.size() == 0))
- return;
- if (!browserShowsCollection())
- fFeedView.refreshTableViewer(true, true);
- else
- fFeedView.refresh(true, true); //TODO Seems some JFace caching problem here (redraw=true)
- }
- });
- }
- });
- /* Done */
- return;
- }
- }
- }
- };
- DynamicDAO.addEntityListener(ISearchMark.class, fSearchMarkListener);
- /* News Listener */
- fNewsListener = new NewsAdapter() {
- /* News got Added */
- @Override
- public void entitiesAdded(final Set<NewsEvent> events) {
- JobRunner.runInUIThread(fFeedView.getEditorControl(), new Runnable() {
- public void run() {
- Set<NewsEvent> addedNews = null;
- /* Filter News which are from a different Feed than displayed */
- for (NewsEvent event : events) {
- if (event.getEntity().isVisible() && isInputRelatedTo(event.getEntity(), EventType.PERSIST)) {
- if (addedNews == null)
- addedNews = new HashSet<NewsEvent>();
- addedNews.add(event);
- }
- /* Return on Shutdown */
- if (Controller.getDefault().isShuttingDown())
- return;
- }
- /* Event not interesting for us or we are disposed */
- if (addedNews == null || addedNews.size() == 0)
- return;
- /* Return on Shutdown */
- if (Controller.getDefault().isShuttingDown())
- return;
- /* Handle */
- boolean refresh = handleAddedNews(addedNews);
- if (refresh) {
- if (!browserShowsCollection())
- fFeedView.refreshTableViewer(true, false);
- else
- fFeedView.refresh(true, false);
- }
- }
- });
- }
- /* News got Updated */
- @Override
- public void entitiesUpdated(final Set<NewsEvent> events) {
- JobRunner.runInUIThread(fFeedView.getEditorControl(), new Runnable() {
- public void run() {
- Set<NewsEvent> restoredNews = null;
- Set<NewsEvent> updatedNews = null;
- Set<NewsEvent> deletedNews = null;
- /* Filter News which are from a different Feed than displayed */
- for (NewsEvent event : events) {
- /* Return on Shutdown */
- if (Controller.getDefault().isShuttingDown())
- return;
- /* Check if input relates to news events */
- if (isInputRelatedTo(event.getEntity(), EventType.UPDATE)) {
- INews news = event.getEntity();
- INews.State oldState = event.getOldNews().getState();
- /* News got Deleted */
- if (!news.isVisible()) {
- if (deletedNews == null)
- deletedNews = new HashSet<NewsEvent>();
- deletedNews.add(event);
- }
- /* News got Restored */
- else if (news.isVisible() && (oldState == INews.State.HIDDEN || oldState == INews.State.DELETED)) {
- if (restoredNews == null)
- restoredNews = new HashSet<NewsEvent>();
- restoredNews.add(event);
- }
- /* News got Updated */
- else {
- if (updatedNews == null)
- updatedNews = new HashSet<NewsEvent>();
- updatedNews.add(event);
- }
- }
- }
- boolean refresh = false;
- boolean updateSelectionFromDelete = false;
- /* Handle Restored News */
- if (restoredNews != null && !restoredNews.isEmpty())
- refresh = handleAddedNews(restoredNews);
- /* Handle Updated News */
- if (updatedNews != null && !updatedNews.isEmpty())
- refresh = handleUpdatedNews(updatedNews);
- /* Handle Deleted News */
- if (deletedNews != null && !deletedNews.isEmpty()) {
- refresh = handleDeletedNews(deletedNews);
- updateSelectionFromDelete = refresh;
- }
- /* Refresh and update selection due to deletion */
- if (updateSelectionFromDelete) {
- fTableViewer.updateSelectionAfterDelete(new Runnable() {
- public void run() {
- refreshViewers(events, EventType.REMOVE);
- }
- });
- }
- /* Normal refresh w/o deletion */
- else if (refresh)
- refreshViewers(events, EventType.UPDATE);
- }
- });
- }
- /* News got Deleted */
- @Override
- public void entitiesDeleted(final Set<NewsEvent> events) {
- JobRunner.runInUIThread(fFeedView.getEditorControl(), new Runnable() {
- public void run() {
- Set<NewsEvent> deletedNews = null;
- /* Filter News which are from a different Feed than displayed */
- for (NewsEvent event : events) {
- INews news = event.getEntity();
- if ((news.isVisible() || news.getParentId() != 0) && isInputRelatedTo(news, EventType.REMOVE)) {
- if (deletedNews == null)
- deletedNews = new HashSet<NewsEvent>();
- deletedNews.add(event);
- }
- }
- /* Return on Shutdown */
- if (Controller.getDefault().isShuttingDown())
- return;
- /* Event not interesting for us or we are disposed */
- if (deletedNews == null || deletedNews.size() == 0)
- return;
- /* Handle Deleted News */
- boolean refresh = handleDeletedNews(deletedNews);
- if (refresh) {
- if (!browserShowsCollection())
- fFeedView.refreshTableViewer(true, false);
- else
- fFeedView.refresh(true, false);
- }
- }
- });
- }
- };
- DynamicDAO.addEntityListener(INews.class, fNewsListener);
- }
- private void refreshViewers(final Set<NewsEvent> events, EventType type) {
- /* Return on Shutdown */
- if (Controller.getDefault().isShuttingDown())
- return;
- /*
- * Optimization: The Browser is likely only showing a single news and thus
- * there is no need to refresh the entire content but rather use the update
- * instead.
- */
- if (!browserShowsCollection()) {
- List<INews> items = new ArrayList<INews>(events.size());
- for (NewsEvent event : events) {
- items.add(event.getEntity());
- }
- /* Update Browser Viewer */
- if (contains(fBrowserViewer.getInput(), items)) {
- /* Update */
- if (type == EventType.UPDATE) {
- Set<NewsEvent> newsToUpdate = events;
- /*
- * Optimization: If more than a single news is to update, check
- * if the Browser only shows a single news to avoid a full refresh.
- */
- if (events.size() > 1) {
- NewsEvent event = findShowingEventFromBrowser(events);
- if (event != null)
- newsToUpdate = Collections.singleton(event);
- }
- fBrowserViewer.update(newsToUpdate);
- }
- /* Remove */
- else if (type == EventType.REMOVE)
- fBrowserViewer.remove(items.toArray());
- }
- /* Refresh Table Viewer */
- fFeedView.refreshTableViewer(true, true);
- }
- /* Browser is showing Collection, thereby perform a refresh */
- else
- fFeedView.refresh(true, true);
- }
- private boolean handleAddedNews(Set<NewsEvent> events) {
- /*
- * Input can be NULL if this listener was called before NewsTableControl.setPartInput()
- * has been called (can happen if the viewer has thousands of items to load)
- */
- if (fFeedView.isTableViewerVisible() && fTableViewer.getInput() == null)
- return false;
- /* Receive added News */
- List<INews> addedNews = new ArrayList<INews>(events.size());
- for (NewsEvent event : events) {
- addedNews.add(event.getEntity());
- }
- /* Add to Cache */
- synchronized (NewsContentProvider.this) {
- fCachedNews.addAll(addedNews);
- }
- /* Return early if a refresh is required anyways */
- if (fGrouping.needsRefresh(INews.class, events, false))
- return true;
- addToViewers(addedNews);
- return false;
- }
- /* Add a List of News to Table and Browser Viewers */
- private void addToViewers(List<INews> addedNews) {
- /* Add to Table-Viewer if Visible (keep top item and selection stable) */
- if (fFeedView.isTableViewerVisible()) {
- Tree tree = fTableViewer.getTree();
- TreeItem topItem = tree.getTopItem();
- int indexOfTopItem = 0;
- if (topItem != null)
- indexOfTopItem = tree.indexOf(topItem);
- tree.setRedraw(false);
- try {
- fTableViewer.add(fTableViewer.getInput(), addedNews.toArray());
- if (topItem != null && indexOfTopItem != 0)
- tree.setTopItem(topItem);
- } finally {
- tree.setRedraw(true);
- }
- }
- /* Add to Browser-Viewer if showing entire Feed */
- if (browserShowsCollection())
- fBrowserViewer.add(fBrowserViewer.getInput(), addedNews.toArray());
- }
- /* Browser shows collection if maximized */
- private boolean browserShowsCollection() {
- Object input = fBrowserViewer.getInput();
- return (input instanceof BookMarkReference || input instanceof NewsBinReference || input instanceof SearchMarkReference || input instanceof FolderNewsMarkReference);
- }
- private boolean handleUpdatedNews(Set<NewsEvent> events) {
- /* Receive updated News */
- List<INews> updatedNews = new ArrayList<INews>(events.size());
- for (NewsEvent event : events) {
- updatedNews.add(event.getEntity());
- }
- /* Return early if refresh is required anyways for Grouper */
- if (fGrouping.needsRefresh(INews.class, events, true))
- return true;
- /* Return early if refresh is required anyways for Filter */
- if (fFilter.needsRefresh(events))
- return true;
- /* Update in Table-Viewer */
- if (fFeedView.isTableViewerVisible())
- fTableViewer.update(updatedNews.toArray(), null);
- /* Update in Browser-Viewer */
- if (contains(fBrowserViewer.getInput(), updatedNews)) {
- Set<NewsEvent> newsToUpdate = events;
- /*
- * Optimization: If more than a single news is to update, check
- * if the Browser only shows a single news to avoid a full refresh.
- */
- if (events.size() > 1) {
- NewsEvent event = findShowingEventFromBrowser(events);
- if (event != null)
- newsToUpdate = Collections.singleton(event);
- }
- fBrowserViewer.update(newsToUpdate);
- }
- return false;
- }
- private boolean handleDeletedNews(Set<NewsEvent> events) {
- /* Receive deleted News */
- List<INews> deletedNews = new ArrayList<INews>(events.size());
- for (NewsEvent event : events) {
- deletedNews.add(event.getEntity());
- }
- /* Remove from Cache */
- synchronized (NewsContentProvider.this) {
- fCachedNews.removeAll(deletedNews);
- }
- /* Return early if refresh is required anyways */
- if (fGrouping.needsRefresh(INews.class, events, false))
- return true;
- /* Grouping Disabled */
- if (!isGroupingEnabled()) {
- /* Remove from Table-Viewer */
- if (fFeedView.isTableViewerVisible())
- fTableViewer.remove(deletedNews.toArray());
- /* Remove from Browser-Viewer */
- if (contains(fBrowserViewer.getInput(), deletedNews))
- fBrowserViewer.remove(deletedNews.toArray());
- }
- return false;
- }
- private void unregisterListeners() {
- DynamicDAO.removeEntityListener(INews.class, fNewsListener);
- DynamicDAO.removeEntityListener(ISearchMark.class, fSearchMarkListener);
- }
- private boolean isInputRelatedTo(INews news, EventType type) {
- /* Check if BookMark references the News' Feed and is not a copy */
- if (fInput instanceof IBookMark) {
- IBookMark bookmark = (IBookMark) fInput;
- if (news.getParentId() == 0 && bookmark.getFeedLinkReference().equals(news.getFeedReference()))
- return true;
- }
- /* Check if Saved Search contains the given News */
- else if (type != EventType.PERSIST && fInput instanceof ISearchMark) {
- /*
- * Workaround a race condition in a safe way: When a News gets updated or deleted from a
- * Searchmark, the Indexer is the first to process this event. Since the SavedSearchService
- * updates all Searchmarks instantly as a result of that, the Searchmark at this point could no
- * longer contain the affected News and isInputRelated() would return false. The fix is
- * to check the cache for the News instead of the potential modified Searchmark.
- */
- return hasCachedNews(news);
- }
- /* Update / Remove: Check if News points to this Bin */
- else if (fInput instanceof INewsBin) {
- return news.getParentId() == fInput.getId();
- }
- /* In Memory Folder News Mark (aggregated news) */
- else if (fInput instanceof FolderNewsMark) {
- /* News Added/Updated: Check if its part of the Folder */
- if (type == EventType.PERSIST || type == EventType.UPDATE) {
- return ((FolderNewsMark) fInput).isRelatedTo(news);
- }
- /* Remove: Check if news was cached */
- return hasCachedNews(news);
- }
- return false;
- }
- private boolean contains(Object input, List<INews> list) {
- /* Can only belong to this Feed since filtered before already */
- if (input instanceof BookMarkReference || input instanceof NewsBinReference || input instanceof SearchMarkReference || input instanceof FolderNewsMarkReference)
- return true;
- else if (input instanceof INews)
- return list.contains(input);
- else if (input instanceof EntityGroup) {
- List<EntityGroupItem> items = ((EntityGroup) input).getItems();
- for (EntityGroupItem item : items) {
- if (list.contains(item.getEntity()))
- return true;
- }
- }
- else if (input instanceof Object[]) {
- Object inputNews[] = (Object[]) input;
- for (Object inputNewsItem : inputNews) {
- if (list.contains(inputNewsItem))
- return true;
- }
- }
- return false;
- }
- private NewsEvent findShowingEventFromBrowser(Set<NewsEvent> events) {
- Object input = fBrowserViewer.getInput();
- if (input instanceof INews) {
- INews news = (INews) input;
- for (NewsEvent event : events) {
- if (news.equals(event.getEntity()))
- return event;
- }
- }
- return null;
- }
- }