| Index: chrome/browser/translate/translate_manager2.cc
|
| diff --git a/chrome/browser/translate/translate_manager2.cc b/chrome/browser/translate/translate_manager2.cc
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..24cc2a6cbd05ceab137e1d7e03785b1978f28829
|
| --- /dev/null
|
| +++ b/chrome/browser/translate/translate_manager2.cc
|
| @@ -0,0 +1,549 @@
|
| +// Copyright (c) 2010 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +#include "chrome/browser/translate/translate_manager2.h"
|
| +
|
| +#include "app/resource_bundle.h"
|
| +#include "base/compiler_specific.h"
|
| +#include "base/string_util.h"
|
| +#include "chrome/browser/browser_process.h"
|
| +#include "chrome/browser/pref_service.h"
|
| +#include "chrome/browser/profile.h"
|
| +#include "chrome/browser/renderer_host/render_process_host.h"
|
| +#include "chrome/browser/renderer_host/render_view_host.h"
|
| +#include "chrome/browser/tab_contents/language_state.h"
|
| +#include "chrome/browser/tab_contents/navigation_controller.h"
|
| +#include "chrome/browser/tab_contents/navigation_entry.h"
|
| +#include "chrome/browser/tab_contents/tab_contents.h"
|
| +#include "chrome/browser/tab_contents/tab_util.h"
|
| +#include "chrome/browser/translate/page_translated_details.h"
|
| +#include "chrome/browser/translate/translate_infobar_delegate2.h"
|
| +#include "chrome/browser/translate/translate_prefs.h"
|
| +#include "chrome/common/notification_details.h"
|
| +#include "chrome/common/notification_service.h"
|
| +#include "chrome/common/notification_source.h"
|
| +#include "chrome/common/notification_type.h"
|
| +#include "chrome/common/pref_names.h"
|
| +#include "chrome/common/translate_errors.h"
|
| +#include "grit/browser_resources.h"
|
| +#include "net/url_request/url_request_status.h"
|
| +
|
| +namespace {
|
| +
|
| +// Mapping from a locale name to a language code name.
|
| +// Locale names not included are translated as is.
|
| +struct LocaleToCLDLanguage {
|
| + const char* locale_language; // Language Chrome locale is in.
|
| + const char* cld_language; // Language the CLD reports.
|
| +};
|
| +LocaleToCLDLanguage kLocaleToCLDLanguages[] = {
|
| + { "en-GB", "en" },
|
| + { "en-US", "en" },
|
| + { "es-419", "es" },
|
| + { "pt-BR", "pt" },
|
| + { "pt-PT", "pt" },
|
| +};
|
| +
|
| +// The list of languages the Google translation server supports.
|
| +// For information, here is the list of languages that Chrome can be run in
|
| +// but that the translation server does not support:
|
| +// am Amharic
|
| +// bn Bengali
|
| +// gu Gujarati
|
| +// kn Kannada
|
| +// ml Malayalam
|
| +// mr Marathi
|
| +// ta Tamil
|
| +// te Telugu
|
| +const char* kSupportedLanguages[] = {
|
| + "af", // Afrikaans
|
| + "sq", // Albanian
|
| + "ar", // Arabic
|
| + "be", // Belarusian
|
| + "bg", // Bulgarian
|
| + "ca", // Catalan
|
| + "zh-CN", // Chinese (Simplified)
|
| + "zh-TW", // Chinese (Traditional)
|
| + "hr", // Croatian
|
| + "cs", // Czech
|
| + "da", // Danish
|
| + "nl", // Dutch
|
| + "en", // English
|
| + "et", // Estonian
|
| + "fi", // Finnish
|
| + "fil", // Filipino
|
| + "fr", // French
|
| + "gl", // Galician
|
| + "de", // German
|
| + "el", // Greek
|
| + "he", // Hebrew
|
| + "hi", // Hindi
|
| + "hu", // Hungarian
|
| + "is", // Icelandic
|
| + "id", // Indonesian
|
| + "it", // Italian
|
| + "ga", // Irish
|
| + "ja", // Japanese
|
| + "ko", // Korean
|
| + "lv", // Latvian
|
| + "lt", // Lithuanian
|
| + "mk", // Macedonian
|
| + "ms", // Malay
|
| + "mt", // Maltese
|
| + "nb", // Norwegian
|
| + "fa", // Persian
|
| + "pl", // Polish
|
| + "pt", // Portuguese
|
| + "ro", // Romanian
|
| + "ru", // Russian
|
| + "sr", // Serbian
|
| + "sk", // Slovak
|
| + "sl", // Slovenian
|
| + "es", // Spanish
|
| + "sw", // Swahili
|
| + "sv", // Swedish
|
| + "th", // Thai
|
| + "tr", // Turkish
|
| + "uk", // Ukrainian
|
| + "vi", // Vietnamese
|
| + "cy", // Welsh
|
| + "yi", // Yiddish
|
| +};
|
| +
|
| +const char* const kTranslateScriptURL =
|
| + "http://translate.google.com/translate_a/element.js?"
|
| + "cb=cr.googleTranslate.onTranslateElementLoad";
|
| +const char* const kTranslateScriptHeader =
|
| + "Google-Translate-Element-Mode: library";
|
| +
|
| +} // namespace
|
| +
|
| +// static
|
| +base::LazyInstance<std::set<std::string> >
|
| + TranslateManager2::supported_languages_(base::LINKER_INITIALIZED);
|
| +
|
| +TranslateManager2::~TranslateManager2() {
|
| +}
|
| +
|
| +// static
|
| +bool TranslateManager2::IsTranslatableURL(const GURL& url) {
|
| + return !url.SchemeIs("chrome");
|
| +}
|
| +
|
| +// static
|
| +void TranslateManager2::GetSupportedLanguages(
|
| + std::vector<std::string>* languages) {
|
| + DCHECK(languages && languages->empty());
|
| + for (size_t i = 0; i < arraysize(kSupportedLanguages); ++i)
|
| + languages->push_back(kSupportedLanguages[i]);
|
| +}
|
| +
|
| +// static
|
| +std::string TranslateManager2::GetLanguageCode(
|
| + const std::string& chrome_locale) {
|
| + for (size_t i = 0; i < arraysize(kLocaleToCLDLanguages); ++i) {
|
| + if (chrome_locale == kLocaleToCLDLanguages[i].locale_language)
|
| + return kLocaleToCLDLanguages[i].cld_language;
|
| + }
|
| + return chrome_locale;
|
| +}
|
| +
|
| +// static
|
| +bool TranslateManager2::IsSupportedLanguage(const std::string& page_language) {
|
| + if (supported_languages_.Pointer()->empty()) {
|
| + for (size_t i = 0; i < arraysize(kSupportedLanguages); ++i)
|
| + supported_languages_.Pointer()->insert(kSupportedLanguages[i]);
|
| + }
|
| + return supported_languages_.Pointer()->find(page_language) !=
|
| + supported_languages_.Pointer()->end();
|
| +}
|
| +
|
| +void TranslateManager2::Observe(NotificationType type,
|
| + const NotificationSource& source,
|
| + const NotificationDetails& details) {
|
| + switch (type.value) {
|
| + case NotificationType::NAV_ENTRY_COMMITTED: {
|
| + NavigationController* controller =
|
| + Source<NavigationController>(source).ptr();
|
| + NavigationController::LoadCommittedDetails* load_details =
|
| + Details<NavigationController::LoadCommittedDetails>(details).ptr();
|
| + NavigationEntry* entry = controller->GetActiveEntry();
|
| + if (!entry) {
|
| + NOTREACHED();
|
| + return;
|
| + }
|
| + if (entry->transition_type() != PageTransition::RELOAD &&
|
| + load_details->type != NavigationType::SAME_PAGE) {
|
| + return;
|
| + }
|
| + // When doing a page reload, we don't get a TAB_LANGUAGE_DETERMINED
|
| + // notification. So we need to explictly initiate the translation.
|
| + // Note that we delay it as the TranslateManager2 gets this notification
|
| + // before the TabContents and the TabContents processing might remove the
|
| + // current infobars. Since InitTranslation might add an infobar, it must
|
| + // be done after that.
|
| + MessageLoop::current()->PostTask(FROM_HERE,
|
| + method_factory_.NewRunnableMethod(
|
| + &TranslateManager2::InitiateTranslationPosted,
|
| + controller->tab_contents()->render_view_host()->process()->id(),
|
| + controller->tab_contents()->render_view_host()->routing_id(),
|
| + controller->tab_contents()->language_state().
|
| + original_language()));
|
| + break;
|
| + }
|
| + case NotificationType::TAB_LANGUAGE_DETERMINED: {
|
| + TabContents* tab = Source<TabContents>(source).ptr();
|
| + // We may get this notifications multiple times. Make sure to translate
|
| + // only once.
|
| + LanguageState& language_state = tab->language_state();
|
| + if (!language_state.translation_pending() &&
|
| + !language_state.translation_declined() &&
|
| + !language_state.IsPageTranslated()) {
|
| + std::string language = *(Details<std::string>(details).ptr());
|
| + InitiateTranslation(tab, language);
|
| + }
|
| + break;
|
| + }
|
| + case NotificationType::PAGE_TRANSLATED: {
|
| + // Only add translate infobar if it doesn't exist; if it already exists,
|
| + // just update the state, the actual infobar would have received the same
|
| + // notification and update the visual display accordingly.
|
| + TabContents* tab = Source<TabContents>(source).ptr();
|
| + PageTranslatedDetails* page_translated_details =
|
| + Details<PageTranslatedDetails>(details).ptr();
|
| + PageTranslated(tab, page_translated_details);
|
| + break;
|
| + }
|
| + case NotificationType::PROFILE_DESTROYED: {
|
| + Profile* profile = Source<Profile>(source).ptr();
|
| + notification_registrar_.Remove(this, NotificationType::PROFILE_DESTROYED,
|
| + source);
|
| + size_t count = accept_languages_.erase(profile->GetPrefs());
|
| + // We should know about this profile since we are listening for
|
| + // notifications on it.
|
| + DCHECK(count > 0);
|
| + profile->GetPrefs()->RemovePrefObserver(prefs::kAcceptLanguages, this);
|
| + break;
|
| + }
|
| + case NotificationType::PREF_CHANGED: {
|
| + DCHECK(*Details<std::wstring>(details).ptr() == prefs::kAcceptLanguages);
|
| + PrefService* prefs = Source<PrefService>(source).ptr();
|
| + InitAcceptLanguages(prefs);
|
| + break;
|
| + }
|
| + default:
|
| + NOTREACHED();
|
| + }
|
| +}
|
| +
|
| +void TranslateManager2::OnURLFetchComplete(const URLFetcher* source,
|
| + const GURL& url,
|
| + const URLRequestStatus& status,
|
| + int response_code,
|
| + const ResponseCookies& cookies,
|
| + const std::string& data) {
|
| + scoped_ptr<const URLFetcher> delete_ptr(source);
|
| + DCHECK(translate_script_request_pending_);
|
| + translate_script_request_pending_ = false;
|
| + bool error =
|
| + (status.status() != URLRequestStatus::SUCCESS || response_code != 200);
|
| +
|
| + if (!error) {
|
| + base::StringPiece str = ResourceBundle::GetSharedInstance().
|
| + GetRawDataResource(IDR_TRANSLATE_JS);
|
| + DCHECK(translate_script_.empty());
|
| + str.CopyToString(&translate_script_);
|
| + translate_script_ += "\n" + data;
|
| + }
|
| +
|
| + // Process any pending requests.
|
| + std::vector<PendingRequest>::const_iterator iter;
|
| + for (iter = pending_requests_.begin(); iter != pending_requests_.end();
|
| + ++iter) {
|
| + const PendingRequest& request = *iter;
|
| + TabContents* tab = tab_util::GetTabContentsByID(request.render_process_id,
|
| + request.render_view_id);
|
| + if (!tab) {
|
| + // The tab went away while we were retrieving the script.
|
| + continue;
|
| + }
|
| + NavigationEntry* entry = tab->controller().GetActiveEntry();
|
| + if (!entry || entry->page_id() != request.page_id) {
|
| + // We navigated away from the page the translation was triggered on.
|
| + continue;
|
| + }
|
| +
|
| + if (error) {
|
| + ShowInfoBar(tab,
|
| + TranslateInfoBarDelegate2::CreateInstance(
|
| + TranslateInfoBarDelegate2::TRANSLATION_ERROR,
|
| + TranslateErrors::NETWORK,
|
| + tab, request.source_lang, request.target_lang));
|
| + } else {
|
| + // Translate the page.
|
| + DoTranslatePage(tab, translate_script_,
|
| + request.source_lang, request.target_lang);
|
| + }
|
| + }
|
| + pending_requests_.clear();
|
| +}
|
| +
|
| +// static
|
| +bool TranslateManager2::IsShowingTranslateInfobar(TabContents* tab) {
|
| + return GetTranslateInfoBarDelegate2(tab) != NULL;
|
| +}
|
| +
|
| +TranslateManager2::TranslateManager2()
|
| + : ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)),
|
| + translate_script_request_pending_(false) {
|
| + notification_registrar_.Add(this, NotificationType::NAV_ENTRY_COMMITTED,
|
| + NotificationService::AllSources());
|
| + notification_registrar_.Add(this, NotificationType::TAB_LANGUAGE_DETERMINED,
|
| + NotificationService::AllSources());
|
| + notification_registrar_.Add(this, NotificationType::PAGE_TRANSLATED,
|
| + NotificationService::AllSources());
|
| +}
|
| +
|
| +void TranslateManager2::InitiateTranslation(TabContents* tab,
|
| + const std::string& page_lang) {
|
| + PrefService* prefs = tab->profile()->GetPrefs();
|
| + if (!prefs->GetBoolean(prefs::kEnableTranslate))
|
| + return;
|
| +
|
| + NavigationEntry* entry = tab->controller().GetActiveEntry();
|
| + if (!entry) {
|
| + // This can happen for popups created with window.open("").
|
| + return;
|
| + }
|
| +
|
| + // If there is already a translate infobar showing, don't show another one.
|
| + if (GetTranslateInfoBarDelegate2(tab))
|
| + return;
|
| +
|
| + std::string target_lang = GetTargetLanguage();
|
| + // Nothing to do if either the language Chrome is in or the language of the
|
| + // page is not supported by the translation server.
|
| + if (target_lang.empty() || !IsSupportedLanguage(page_lang)) {
|
| + return;
|
| + }
|
| +
|
| + // We don't want to translate:
|
| + // - any Chrome specific page (New Tab Page, Download, History... pages).
|
| + // - similar languages (ex: en-US to en).
|
| + // - any user black-listed URLs or user selected language combination.
|
| + // - any language the user configured as accepted languages.
|
| + if (!IsTranslatableURL(entry->url()) || page_lang == target_lang ||
|
| + !TranslatePrefs::CanTranslate(prefs, page_lang, entry->url()) ||
|
| + IsAcceptLanguage(tab, page_lang)) {
|
| + return;
|
| + }
|
| +
|
| + // If the user has previously selected "always translate" for this language we
|
| + // automatically translate. Note that in incognito mode we disable that
|
| + // feature; the user will get an infobar, so they can control whether the
|
| + // page's text is sent to the translate server.
|
| + std::string auto_target_lang;
|
| + if (!tab->profile()->IsOffTheRecord() &&
|
| + TranslatePrefs::ShouldAutoTranslate(prefs, page_lang,
|
| + &auto_target_lang)) {
|
| + TranslatePage(tab, page_lang, auto_target_lang);
|
| + return;
|
| + }
|
| +
|
| + std::string auto_translate_to = tab->language_state().AutoTranslateTo();
|
| + if (!auto_translate_to.empty()) {
|
| + // This page was navigated through a click from a translated page.
|
| + TranslatePage(tab, page_lang, auto_translate_to);
|
| + return;
|
| + }
|
| +
|
| + // Prompts the user if he/she wants the page translated.
|
| + tab->AddInfoBar(TranslateInfoBarDelegate2::CreateInstance(
|
| + TranslateInfoBarDelegate2::BEFORE_TRANSLATE,
|
| + TranslateErrors::NONE, tab, page_lang, target_lang));
|
| +}
|
| +
|
| +void TranslateManager2::InitiateTranslationPosted(
|
| + int process_id, int render_id, const std::string& page_lang) {
|
| + // The tab might have been closed.
|
| + TabContents* tab = tab_util::GetTabContentsByID(process_id, render_id);
|
| + if (!tab || tab->language_state().translation_pending())
|
| + return;
|
| +
|
| + InitiateTranslation(tab, page_lang);
|
| +}
|
| +
|
| +void TranslateManager2::TranslatePage(TabContents* tab_contents,
|
| + const std::string& source_lang,
|
| + const std::string& target_lang) {
|
| + NavigationEntry* entry = tab_contents->controller().GetActiveEntry();
|
| + if (!entry) {
|
| + NOTREACHED();
|
| + return;
|
| + }
|
| + if (!translate_script_.empty()) {
|
| + DoTranslatePage(tab_contents, translate_script_, source_lang, target_lang);
|
| + return;
|
| + }
|
| +
|
| + // The script is not available yet. Queue that request and query for the
|
| + // script. Once it is downloaded we'll do the translate.
|
| + RenderViewHost* rvh = tab_contents->render_view_host();
|
| + PendingRequest request;
|
| + request.render_process_id = rvh->process()->id();
|
| + request.render_view_id = rvh->routing_id();
|
| + request.page_id = entry->page_id();
|
| + request.source_lang = source_lang;
|
| + request.target_lang = target_lang;
|
| + pending_requests_.push_back(request);
|
| + RequestTranslateScript();
|
| +}
|
| +
|
| +void TranslateManager2::RevertTranslation(TabContents* tab_contents) {
|
| + NavigationEntry* entry = tab_contents->controller().GetActiveEntry();
|
| + if (!entry) {
|
| + NOTREACHED();
|
| + return;
|
| + }
|
| + tab_contents->render_view_host()->RevertTranslation(entry->page_id());
|
| + tab_contents->language_state().set_current_language(
|
| + tab_contents->language_state().original_language());
|
| +}
|
| +
|
| +void TranslateManager2::DoTranslatePage(TabContents* tab,
|
| + const std::string& translate_script,
|
| + const std::string& source_lang,
|
| + const std::string& target_lang) {
|
| + NavigationEntry* entry = tab->controller().GetActiveEntry();
|
| + if (!entry) {
|
| + NOTREACHED();
|
| + return;
|
| + }
|
| +
|
| + TranslateInfoBarDelegate2* infobar = GetTranslateInfoBarDelegate2(tab);
|
| + if (infobar) {
|
| + // We don't show the translating infobar if no translate infobar is already
|
| + // showing (that is the case when the translation was triggered by the
|
| + // "always translate" for example).
|
| + infobar = TranslateInfoBarDelegate2::CreateInstance(
|
| + TranslateInfoBarDelegate2::TRANSLATING, TranslateErrors::NONE,
|
| + tab, source_lang, target_lang);
|
| + ShowInfoBar(tab, infobar);
|
| + }
|
| + tab->language_state().set_translation_pending(true);
|
| + tab->render_view_host()->TranslatePage(entry->page_id(), translate_script,
|
| + source_lang, target_lang);
|
| +}
|
| +
|
| +void TranslateManager2::PageTranslated(TabContents* tab,
|
| + PageTranslatedDetails* details) {
|
| + // Create the new infobar to display.
|
| + TranslateInfoBarDelegate2* infobar;
|
| + if (details->error_type != TranslateErrors::NONE) {
|
| + infobar = TranslateInfoBarDelegate2::CreateInstance(
|
| + TranslateInfoBarDelegate2::TRANSLATION_ERROR, details->error_type,
|
| + tab, details->source_language, details->target_language);
|
| + } else {
|
| + infobar = TranslateInfoBarDelegate2::CreateInstance(
|
| + TranslateInfoBarDelegate2::AFTER_TRANSLATE, TranslateErrors::NONE,
|
| + tab, details->source_language, details->target_language);
|
| + }
|
| + ShowInfoBar(tab, infobar);
|
| +}
|
| +
|
| +bool TranslateManager2::IsAcceptLanguage(TabContents* tab,
|
| + const std::string& language) {
|
| + PrefService* pref_service = tab->profile()->GetPrefs();
|
| + PrefServiceLanguagesMap::const_iterator iter =
|
| + accept_languages_.find(pref_service);
|
| + if (iter == accept_languages_.end()) {
|
| + InitAcceptLanguages(pref_service);
|
| + // Listen for this profile going away, in which case we would need to clear
|
| + // the accepted languages for the profile.
|
| + notification_registrar_.Add(this, NotificationType::PROFILE_DESTROYED,
|
| + Source<Profile>(tab->profile()));
|
| + // Also start listening for changes in the accept languages.
|
| + tab->profile()->GetPrefs()->AddPrefObserver(prefs::kAcceptLanguages, this);
|
| +
|
| + iter = accept_languages_.find(pref_service);
|
| + }
|
| +
|
| + return iter->second.count(language) != 0;
|
| +}
|
| +
|
| +void TranslateManager2::InitAcceptLanguages(PrefService* prefs) {
|
| + // We have been asked for this profile, build the languages.
|
| + std::wstring accept_langs_str = prefs->GetString(prefs::kAcceptLanguages);
|
| + std::vector<std::string> accept_langs_list;
|
| + LanguageSet accept_langs_set;
|
| + SplitString(WideToASCII(accept_langs_str), ',', &accept_langs_list);
|
| + std::vector<std::string>::const_iterator iter;
|
| + std::string ui_lang =
|
| + GetLanguageCode(g_browser_process->GetApplicationLocale());
|
| + bool is_ui_english = StartsWithASCII(ui_lang, "en-", false);
|
| + for (iter = accept_langs_list.begin();
|
| + iter != accept_langs_list.end(); ++iter) {
|
| + // Get rid of the locale extension if any (ex: en-US -> en), but for Chinese
|
| + // for which the CLD reports zh-CN and zh-TW.
|
| + std::string accept_lang(*iter);
|
| + size_t index = iter->find("-");
|
| + if (index != std::string::npos && *iter != "zh-CN" && *iter != "zh-TW")
|
| + accept_lang = iter->substr(0, index);
|
| + // Special-case English until we resolve bug 36182 properly.
|
| + // Add English only if the UI language is not English. This will annoy
|
| + // users of non-English Chrome who can comprehend English until English is
|
| + // black-listed.
|
| + // TODO(jungshik): Once we determine that it's safe to remove English from
|
| + // the default Accept-Language values for most locales, remove this
|
| + // special-casing.
|
| + if (accept_lang != "en" || is_ui_english)
|
| + accept_langs_set.insert(accept_lang);
|
| + }
|
| + accept_languages_[prefs] = accept_langs_set;
|
| +}
|
| +
|
| +void TranslateManager2::RequestTranslateScript() {
|
| + if (translate_script_request_pending_)
|
| + return;
|
| +
|
| + translate_script_request_pending_ = true;
|
| + URLFetcher* fetcher = URLFetcher::Create(0, GURL(kTranslateScriptURL),
|
| + URLFetcher::GET, this);
|
| + fetcher->set_request_context(Profile::GetDefaultRequestContext());
|
| + fetcher->set_extra_request_headers(kTranslateScriptHeader);
|
| + fetcher->Start();
|
| +}
|
| +
|
| +void TranslateManager2::ShowInfoBar(TabContents* tab,
|
| + TranslateInfoBarDelegate2* infobar) {
|
| + TranslateInfoBarDelegate2* old_infobar = GetTranslateInfoBarDelegate2(tab);
|
| + infobar->UpdateBackgroundAnimation(old_infobar);
|
| + if (old_infobar) {
|
| + // There already is a translate infobar, simply replace it.
|
| + tab->ReplaceInfoBar(old_infobar, infobar);
|
| + } else {
|
| + tab->AddInfoBar(infobar);
|
| + }
|
| +}
|
| +
|
| +// static
|
| +std::string TranslateManager2::GetTargetLanguage() {
|
| + std::string target_lang =
|
| + GetLanguageCode(g_browser_process->GetApplicationLocale());
|
| + if (IsSupportedLanguage(target_lang))
|
| + return target_lang;
|
| + return std::string();
|
| +}
|
| +
|
| +// static
|
| +TranslateInfoBarDelegate2* TranslateManager2::GetTranslateInfoBarDelegate2(
|
| + TabContents* tab) {
|
| + for (int i = 0; i < tab->infobar_delegate_count(); ++i) {
|
| + TranslateInfoBarDelegate2* delegate =
|
| + tab->GetInfoBarDelegateAt(i)->AsTranslateInfoBarDelegate2();
|
| + if (delegate)
|
| + return delegate;
|
| + }
|
| + return NULL;
|
| +}
|
|
|