templateManager->getTemplate('core', 'layout.user'); if (in_array(\OC_App::getCurrentApp(), ['settings','admin', 'help']) !== false) { $page->assign('bodyid', 'body-settings'); } else { $page->assign('bodyid', 'body-user'); } $this->initialState->provideInitialState('core', 'active-app', $this->navigationManager->getActiveEntry()); $this->initialState->provideInitialState('core', 'apps', array_values($this->navigationManager->getAll())); if ($this->config->getSystemValueBool('unified_search.enabled', false) || !$this->config->getSystemValueBool('enable_non-accessible_features', true)) { $this->initialState->provideInitialState('unified-search', 'limit-default', (int)$this->config->getAppValue('core', 'unified-search.limit-default', (string)SearchQuery::LIMIT_DEFAULT)); $this->initialState->provideInitialState('unified-search', 'min-search-length', (int)$this->config->getAppValue('core', 'unified-search.min-search-length', (string)1)); $this->initialState->provideInitialState('unified-search', 'live-search', $this->config->getAppValue('core', 'unified-search.live-search', 'yes') === 'yes'); Util::addScript('core', 'legacy-unified-search', 'core'); } else { Util::addScript('core', 'unified-search', 'core'); } // Set logo link target $logoUrl = $this->config->getSystemValueString('logo_url', ''); $page->assign('logoUrl', $logoUrl); // Set default entry name $defaultEntryId = $this->navigationManager->getDefaultEntryIdForUser(); $defaultEntry = $this->navigationManager->get($defaultEntryId); $page->assign('defaultAppName', $defaultEntry['name'] ?? ''); // Add navigation entry $page->assign('application', ''); $page->assign('appid', $appId); $navigation = $this->navigationManager->getAll(); $page->assign('navigation', $navigation); $settingsNavigation = $this->navigationManager->getAll('settings'); $this->initialState->provideInitialState('core', 'settingsNavEntries', $settingsNavigation); foreach ($navigation as $entry) { if ($entry['active']) { $page->assign('application', $entry['name']); break; } } foreach ($settingsNavigation as $entry) { if ($entry['active']) { $page->assign('application', $entry['name']); break; } } $user = Server::get(IUserSession::class)->getUser(); if ($user === null) { $page->assign('user_uid', false); $page->assign('user_displayname', false); $page->assign('userAvatarSet', false); $page->assign('userStatus', false); } else { $page->assign('user_uid', $user->getUID()); $page->assign('user_displayname', $user->getDisplayName()); $page->assign('userAvatarSet', true); $page->assign('userAvatarVersion', $this->config->getUserValue($user->getUID(), 'avatar', 'version', 0)); } break; case TemplateResponse::RENDER_AS_ERROR: $page = $this->templateManager->getTemplate('core', 'layout.guest', '', false); $page->assign('bodyid', 'body-login'); $page->assign('user_displayname', ''); $page->assign('user_uid', ''); break; case TemplateResponse::RENDER_AS_GUEST: $page = $this->templateManager->getTemplate('core', 'layout.guest'); Util::addStyle('guest'); $page->assign('bodyid', 'body-login'); $userDisplayName = false; $user = Server::get(IUserSession::class)->getUser(); if ($user) { $userDisplayName = $user->getDisplayName(); } $page->assign('user_displayname', $userDisplayName); $page->assign('user_uid', \OC_User::getUser()); break; case TemplateResponse::RENDER_AS_PUBLIC: $page = $this->templateManager->getTemplate('core', 'layout.public'); $page->assign('appid', $appId); $page->assign('bodyid', 'body-public'); // Set logo link target $logoUrl = $this->config->getSystemValueString('logo_url', ''); $page->assign('logoUrl', $logoUrl); $subscription = Server::get(IRegistry::class); $showSimpleSignup = $this->config->getSystemValueBool('simpleSignUpLink.shown', true); if ($showSimpleSignup && $subscription->delegateHasValidSubscription()) { $showSimpleSignup = false; } $defaultSignUpLink = 'https://nextcloud.com/signup/'; $signUpLink = $this->config->getSystemValueString('registration_link', $defaultSignUpLink); if ($signUpLink !== $defaultSignUpLink) { $showSimpleSignup = true; } if ($this->appManager->isEnabledForUser('registration')) { $urlGenerator = Server::get(IURLGenerator::class); $signUpLink = $urlGenerator->getAbsoluteURL('/index.php/apps/registration/'); } $page->assign('showSimpleSignUpLink', $showSimpleSignup); $page->assign('signUpLink', $signUpLink); break; default: $page = $this->templateManager->getTemplate('core', 'layout.base'); break; } // Send the language, locale, and direction to our layouts $l10nFactory = Server::get(IFactory::class); $lang = $l10nFactory->findLanguage(); $locale = $l10nFactory->findLocale($lang); $direction = $l10nFactory->getLanguageDirection($lang); $lang = str_replace('_', '-', $lang); $page->assign('language', $lang); $page->assign('locale', $locale); $page->assign('direction', $direction); // Set body data-theme try { $themesService = Server::get(\OCA\Theming\Service\ThemesService::class); } catch (\Exception) { $themesService = null; } $page->assign('enabledThemes', $themesService?->getEnabledThemes() ?? []); if ($this->config->getSystemValueBool('installed', false)) { if (empty(self::$versionHash)) { $v = $this->appManager->getAppInstalledVersions(); $v['core'] = implode('.', $this->serverVersion->getVersion()); self::$versionHash = substr(md5(implode(',', $v)), 0, 8); } } else { self::$versionHash = md5('not installed'); } // Add the js files $jsFiles = self::findJavascriptFiles(Util::getScripts()); $page->assign('jsfiles', []); if ($this->config->getSystemValueBool('installed', false) && $renderAs != TemplateResponse::RENDER_AS_ERROR) { // this is on purpose outside of the if statement below so that the initial state is prefilled (done in the getConfig() call) // see https://github.com/nextcloud/server/pull/22636 for details $jsConfigHelper = new JSConfigHelper( $this->serverVersion, \OCP\Util::getL10N('lib'), \OCP\Server::get(Defaults::class), $this->appManager, \OC::$server->getSession(), \OC::$server->getUserSession()->getUser(), $this->config, \OC::$server->getGroupManager(), \OC::$server->get(IniGetWrapper::class), \OC::$server->getURLGenerator(), \OC::$server->get(CapabilitiesManager::class), \OCP\Server::get(IInitialStateService::class), \OCP\Server::get(IProvider::class), \OCP\Server::get(FilenameValidator::class), ); $config = $jsConfigHelper->getConfig(); if (\OC::$server->getContentSecurityPolicyNonceManager()->browserSupportsCspV3()) { $page->assign('inline_ocjs', $config); } else { $page->append('jsfiles', \OC::$server->getURLGenerator()->linkToRoute('core.OCJS.getConfig', ['v' => self::$versionHash])); } } foreach ($jsFiles as $info) { $web = $info[1]; $file = $info[2]; $page->append('jsfiles', $web . '/' . $file . $this->getVersionHashSuffix()); } $request = \OCP\Server::get(IRequest::class); try { $pathInfo = $request->getPathInfo(); } catch (\Exception $e) { $pathInfo = ''; } // Do not initialise scss appdata until we have a fully installed instance // Do not load scss for update, errors, installation or login page if ($this->config->getSystemValueBool('installed', false) && !\OCP\Util::needUpgrade() && $pathInfo !== '' && !preg_match('/^\/login/', $pathInfo) && $renderAs !== TemplateResponse::RENDER_AS_ERROR ) { $cssFiles = self::findStylesheetFiles(\OC_Util::$styles); } else { // If we ignore the scss compiler, // we need to load the guest css fallback Util::addStyle('guest'); $cssFiles = self::findStylesheetFiles(\OC_Util::$styles); } $page->assign('cssfiles', []); $page->assign('printcssfiles', []); $this->initialState->provideInitialState('core', 'versionHash', self::$versionHash); foreach ($cssFiles as $info) { $web = $info[1]; $file = $info[2]; if (str_ends_with($file, 'print.css')) { $page->append('printcssfiles', $web . '/' . $file . $this->getVersionHashSuffix()); } else { $suffix = $this->getVersionHashSuffix($web, $file); if (!str_contains($file, '?v=')) { $page->append('cssfiles', $web . '/' . $file . $suffix); } else { $page->append('cssfiles', $web . '/' . $file . '-' . substr($suffix, 3)); } } } if ($request->isUserAgent([Request::USER_AGENT_CLIENT_IOS, Request::USER_AGENT_SAFARI, Request::USER_AGENT_SAFARI_MOBILE])) { // Prevent auto zoom with iOS but still allow user zoom // On chrome (and others) this does not work (will also disable user zoom) $page->assign('viewport_maximum_scale', '1.0'); } $page->assign('initialStates', $this->initialState->getInitialStates()); $page->assign('id-app-content', $renderAs === TemplateResponse::RENDER_AS_USER ? '#app-content' : '#content'); $page->assign('id-app-navigation', $renderAs === TemplateResponse::RENDER_AS_USER ? '#app-navigation' : null); return $page; } protected function getVersionHashSuffix(string $path = '', string $file = ''): string { if ($this->config->getSystemValueBool('debug', false)) { // allows chrome workspace mapping in debug mode return ''; } if ($this->config->getSystemValueBool('installed', false) === false) { // if not installed just return the version hash return '?v=' . self::$versionHash; } $hash = false; // Try the web-root first if ($path !== '') { $hash = $this->getVersionHashByPath($path); } // If not found try the file if ($hash === false && $file !== '') { $hash = $this->getVersionHashByPath($file); } // As a last resort we use the server version hash if ($hash === false) { $hash = self::$versionHash; } // The theming app is force-enabled thus the cache buster is always available $themingSuffix = '-' . $this->config->getAppValue('theming', 'cachebuster', '0'); return '?v=' . $hash . $themingSuffix; } private function getVersionHashByPath(string $path): string|false { if (array_key_exists($path, self::$cacheBusterCache) === false) { // Not yet cached, so lets find the cache buster string $appId = $this->getAppNamefromPath($path); if ($appId === false) { // No app Id could be guessed return false; } if ($appId === 'core') { // core is not a real app but the server itself $hash = self::$versionHash; } else { $appVersion = $this->appManager->getAppVersion($appId); // For shipped apps the app version is not a single source of truth, we rather also need to consider the Nextcloud version if ($this->appManager->isShipped($appId)) { $appVersion .= '-' . self::$versionHash; } $hash = substr(md5($appVersion), 0, 8); } self::$cacheBusterCache[$path] = $hash; } return self::$cacheBusterCache[$path]; } public static function findStylesheetFiles(array $styles): array { if (!self::$cssLocator) { self::$cssLocator = \OCP\Server::get(CSSResourceLocator::class); } self::$cssLocator->find($styles); return self::$cssLocator->getResources(); } public function getAppNamefromPath(string $path): string|false { if ($path !== '') { $pathParts = explode('/', $path); if ($pathParts[0] === 'css') { // This is a scss request return $pathParts[1]; } elseif ($pathParts[0] === 'core') { return 'core'; } return end($pathParts); } return false; } public static function findJavascriptFiles(array $scripts): array { if (!self::$jsLocator) { self::$jsLocator = \OCP\Server::get(JSResourceLocator::class); } self::$jsLocator->find($scripts); return self::$jsLocator->getResources(); } /** * Converts the absolute file path to a relative path from \OC::$SERVERROOT * @param string $filePath Absolute path * @return string Relative path * @throws \Exception If $filePath is not under \OC::$SERVERROOT */ public static function convertToRelativePath(string $filePath) { $relativePath = explode(\OC::$SERVERROOT, $filePath); if (count($relativePath) !== 2) { throw new \Exception('$filePath is not under the \OC::$SERVERROOT'); } return $relativePath[1]; } } 3 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
---
title: Shared State
order: 5
layout: page
---
[[gwt.shared-state]]
= Shared State
The basic communication from a server-side component to its the client-side
widget counterpart is handled using a __shared state__. The shared state is
serialized transparently. It should be considered read-only on the client-side,
as it is not serialized back to the server-side.
A shared state object simply needs to extend the
[classname]#AbstractComponentState#. The member variables should normally be
declared as public.
----
public class MyComponentState extends AbstractComponentState {
public String text;
}
----
A shared state should never contain any logic. If the members have private
visibility for some reason, you can also use public setters and getters, in
which case the property must not be public.
[[gwt.shared-state.location]]
== Location of Shared-State Classes
The shared-state classes are used by both server- and client-side classes, but
widget set compilation requires that they must be located in a client-side
source package. The default location is under a [filename]#client# package under
the package of the [filename]#.gwt.xml# descriptor. If you wish to organize the
shared classes separately from other client-side code, you can define separate
client-side source packages for pure client-side classes and any shared classes.
In addition to shared state classes, shared classes could include enumerations
and other classes needed by shared-state or RPC communication.
For example, you could have the following definitions in the
[filename]#.gwt.xml# descriptor:
----
<source path="client" />
<source path="shared" />
----
The paths are relative to the package containing the descriptor.
[[gwt.shared-state.component]]
== Accessing Shared State on Server-Side
A server-side component can access the shared state with the
[methodname]#getState()# method. It is required that you override the base
implementation with one that returns the shared state object cast to the proper
type, as follows:
----
@Override
public MyComponentState getState() {
return (MyComponentState) super.getState();
}
----
You can then use the [methodname]#getState()# to access the shared state object
with the proper type.
----
public MyComponent() {
getState().setText("This is the initial state");
....
}
----
[[gwt.shared-state.connector]]
== Handling Shared State in a Connector
A connector can access a shared state with the [methodname]#getState()# method.
The access should be read-only. It is required that you override the base
implementation with one that returns the proper shared state type, as follows:
----
@Override
public MyComponentState getState() {
return (MyComponentState) super.getState();
}
----
State changes made on the server-side are communicated transparently to the
client-side. When a state change occurs, the [methodname]#onStateChanged()#
method in the connector is called. You should always call the superclass
method before anything else to handle changes to common component properties.
----
@Override
public void onStateChanged(StateChangeEvent stateChangeEvent) {
super.onStateChanged(stateChangeEvent);
// Copy the state properties to the widget properties
final String text = getState().getText();
getWidget().setText(text);
}
----
The crude [methodname]#onStateChanged()# method is called when any of the state
properties is changed, allowing you to have even complex logic in how you
manipulate the widget according to the state changes. In most cases, however,
you can handle the property changes more easily and also more efficiently by
using instead the [classname]#@OnStateChange# annotation on the handler methods
for each property, as described next in <<gwt.shared-state.onstatechange>>, or
by delegating the property value directly to the widget, as described in
<<gwt.shared-state.delegatetowidget>>.
ifdef::web[]
The processing phases of state changes are described in more detail in
<<dummy/../../../framework/gwt/gwt-advanced#gwt.advanced.phases,"Client-Side
Processing Phases">>.
endif::web[]
[[gwt.shared-state.onstatechange]]
== Handling Property State Changes with [classname]#@OnStateChange#
The [classname]#@OnStateChange# annotation can be used to mark a connector
method that handles state change on a particular property, given as parameter
for the annotation. In addition to higher clarity, this avoids handling all
property changes if a state change occurs in only one or some of them. However,
if a state change can occur in multiple properties, you can only use this
technique if the properties do not have interaction that prevents handling them
separately in arbitrary order.
We can replace the [methodname]#onStateChange()# method in the earlier connector
example with the following:
----
@OnStateChange("text")
void updateText() {
getWidget().setText(getState().text);
}
----
If the shared state property and the widget property have same name and do not
require any type conversion, as is the case in the above example, you could
simplify this even further by using the [classname]#@DelegateToWidget#
annotation for the shared state property, as described in
<<gwt.shared-state.delegatetowidget>>.
[[gwt.shared-state.delegatetowidget]]
== Delegating State Properties to Widget
The [classname]#@DelegateToWidget# annotation for a shared state property
defines automatic delegation of the property value to the corresponding widget
property of the same name and type, by calling the respective setter for the
property in the widget.
----
public class MyComponentState extends AbstractComponentState {
@DelegateToWidget
public String text;
}
----
This is equivalent to handling the state change in the connector, as done in the
example in <<gwt.shared-state.onstatechange>>.
If you want to delegate a shared state property to a widget property of another
name, you can give the property name as a string parameter for the annotation.
----
public class MyComponentState extends AbstractComponentState {
@DelegateToWidget("description")
public String text;
}
----
[[gwt.shared-state.referring]]
== Referring to Components in Shared State
While you can pass any regular Java objects through a shared state, referring to
another component requires special handling because on the server-side you can
only refer to a server-side component, while on the client-side you only have
widgets. References to components can be made by referring to their connectors
(all server-side components implement the [interfacename]#Connector# interface).
----
public class MyComponentState extends AbstractComponentState {
public Connector otherComponent;
}
----
You could then access the component on the server-side as follows:
----
public class MyComponent {
public void MyComponent(Component otherComponent) {
getState().otherComponent = otherComponent;
}
public Component getOtherComponent() {
return (Component)getState().otherComponent;
}
// And the cast method
@Override
public MyComponentState getState() {
return (MyComponentState) super.getState();
}
}
----
On the client-side, you should cast it in a similar fashion to a
[classname]#ComponentConnector#, or possibly to the specific connector type if
it is known.
[[gwt.shared-state.resource]]
== Sharing Resources
Resources, which commonly are references to icons or other images, are another
case of objects that require special handling. A [interfacename]#Resource#
object exists only on the server-side and on the client-side you have an URL to
the resource. You need to use the [methodname]#setResource()# and
[methodname]#getResource()# on the server-side to access a resource, which is
serialized to the client-side separately.
Let us begin with the server-side API:
----
public class MyComponent extends AbstractComponent {
...
public void setMyIcon(Resource myIcon) {
setResource("myIcon", myIcon);
}
public Resource getMyIcon() {
return getResource("myIcon");
}
}
----
On the client-side, you can then get the URL of the resource with
[methodname]#getResourceUrl()#.
----
@Override
public void onStateChanged(StateChangeEvent stateChangeEvent) {
super.onStateChanged(stateChangeEvent);
...
// Get the resource URL for the icon
getWidget().setMyIcon(getResourceUrl("myIcon"));
}
----
The widget could then use the URL, for example, as follows:
----
public class MyWidget extends Label {
...
Element imgElement = null;
public void setMyIcon(String url) {
if (imgElement == null) {
imgElement = DOM.createImg();
getElement().appendChild(imgElement);
}
DOM.setElementAttribute(imgElement, "src", url);
}
}
----