Mercurial Hosting > nabble
diff src/nabble/model/DbSiteUpdater.jmp @ 0:7ecd1a4ef557
add content
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Thu, 21 Mar 2019 19:15:52 -0600 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/nabble/model/DbSiteUpdater.jmp Thu Mar 21 19:15:52 2019 -0600 @@ -0,0 +1,887 @@ +<% +package nabble.model; + +import fschmidt.db.DbDatabase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + + +final class DbSiteUpdater { + private static final Logger logger = LoggerFactory.getLogger(DbSiteUpdater.class); + + // don't forget to update site.schema + static final int version = 167; + + private static class Result { + private static final long wait = 2000L; // 2 seconds + private final long waitUntil = System.currentTimeMillis() + wait; + private DbSiteUpdater updater; + private SQLException sqlException = null; + private RuntimeException runtimeException = null; + private Error error = null; + private final String user; + + private Result(DbDatabase db,String user) { + updater = new DbSiteUpdater(db,this,user); + this.user = user; + updater.start(); + } + + private synchronized void check() throws SQLException { + if( !isDone() ) { + long now = System.currentTimeMillis(); + if( now < waitUntil ) { + try { + wait( waitUntil - now ); + } catch(InterruptedException e) { + throw new RuntimeException(e); + } + } + if( !isDone() ) + throw new UpdatingException(user); + } + if( sqlException != null ) + throw sqlException; + if( runtimeException != null ) + throw runtimeException; + if( error != null ) + throw error; + } + + synchronized boolean isDone() { + return updater==null; + } + + synchronized void done(Throwable e) { + updater = null; + if( e != null ) { + if( e instanceof SQLException ) { + sqlException = (SQLException)e; + } else if( e instanceof RuntimeException ) { + runtimeException = (RuntimeException)e; + } else if( e instanceof Error ) { + error = (Error)e; + } else { + runtimeException = new RuntimeException("invalid exception type",e); + } + } + notifyAll(); + } + } + + private static final Map<String,Result> userResults = new HashMap<String,Result>(); + private static final Object lock = new Object(); + + static void update(String user,DbDatabase db) throws SQLException { + if( user.equals("global") ) + return; // done from command line + getResult(user,db).check(); + } + + private static Result getResult(String user,DbDatabase db) throws SQLException { + synchronized(lock) { + Result result = userResults.get(user); + if( result == null ) { + result = new Result(db,user); + userResults.put(user,result); + } + return result; + } + } + + // for shell + public static void clearResult(long siteId) { + String user = SiteKey.getInstance(siteId).schema(); + synchronized(lock) { + userResults.remove(user); + } + } + + + + + private final Result result; + private final DbUpdater up; + + private DbSiteUpdater(DbDatabase db,Result result,String schema) { + this.up = new DbUpdater(db,schema); + this.result = result; + } + + private void start() { + try { // needed for site creation because version is only up to date in current transaction + if( up.dbVersion() == version ) { + result.done(null); + return; + } + } catch(RuntimeException e) { + result.done(e); + return; + } catch(Error e) { + result.done(e); + return; + } + Executors.executeNow(new Runnable(){public void run(){ + try { + go(); + result.done(null); + } catch(SQLException e) { + result.done(e); + } catch(RuntimeException e) { + result.done(e); + } catch(Error e) { + result.done(e); + } finally { + if( !result.isDone() ) + logger.error("result isn't done"); + } + }}); + } + + void go() throws SQLException { + try { + update(); + if( up.databaseVersion() != version ) + throw new RuntimeException(); + } finally { + up.close(); + } + } + + @Override public String toString() { + return "DbSiteUpdater-" + up.schema; + } + + private void execCatching(String sql,String exStr) throws SQLException { + up.begin(); + try { + up.exec(sql); + } catch(SQLException e) { + if( !e.getMessage().contains(exStr) ) + throw e; + logger.info(e.toString()); + up.exec("rollback"); + } + up.commit(); + } + + void update() throws SQLException { + switch( up.dbVersion() ) { + case 0: + up.execStep( %> + create index embed_key_site_idx on embed_key(site_id) + <% ); + case 1: + up.execStep( %> + create index file_avatar_site_idx on file_avatar(site_id) + <% ); + case 2: + up.execStep( %> + create index file_node_site_idx on file_node(site_id) + <% ); + case 3: + up.execStep( %> + create index file_temp_site_idx on file_temp(site_id) + <% ); + case 4: + up.execStep( %> + create index mailing_list_site_idx on mailing_list(site_id) + <% ); + case 5: + up.execStep( %> + create index node_msg_site_idx on node_msg(site_id) + <% ); + case 6: + up.execStep( %> + create index node_property_site_idx on node_property(site_id) + <% ); + case 7: + up.execStep( %> + create index paid_site_idx on paid(site_id) + <% ); + case 8: + up.execStep( %> + create index registration_site_idx on registration(site_id) + <% ); + case 9: + up.execStep( %> + create index subscription_site_idx on subscription(site_id) + <% ); + case 10: + up.execStep( %> + create index user_property_site_idx on user_property(site_id) + <% ); + case 11: + up.execStep( %> + create index view_count_site_idx on view_count(site_id) + <% ); + case 12: + up.execStep( %> + create index visited_site_idx on visited(site_id) + <% ); + + case 13: + up.execStep( %> + create index tag_node_idx on tag(node_id) + <% ); + case 14: + up.execStep( %> + create index tag_user_idx on tag(user_id) + <% ); + case 15: + up.execStep( %> + create index embed_key_node_idx on embed_key(node_id) + <% ); + case 16: + up.execStep( %> + create index visited_node_idx on visited(node_id) + <% ); + case 17: + up.execStep( %> + create index visited_last_node_idx on visited(last_node_id) + <% ); + case 18: + up.execStep( %> + alter table node add column poll_option_count integer default null + <% ); + case 19: + up.execStep( %> + alter table configuration drop column site_id + <% ); + case 20: + up.execStep( %> + alter table configuration ADD PRIMARY KEY (name) + <% ); + case 21: + up.execStep( %> + alter table embed_key drop column site_id + <% ); + case 22: + up.execStep( %> + alter table file_avatar drop column site_id + <% ); + case 23: + up.execStep( %> + alter table file_node drop column site_id + <% ); + case 24: + up.execStep( %> + alter table file_temp drop column site_id + <% ); + case 25: + up.execStep( %> + alter table mailing_list drop column site_id + <% ); + case 26: + up.execStep( %> + alter table node_msg drop column site_id + <% ); + case 27: + up.execStep( %> + alter table node_property drop column site_id + <% ); + case 28: + up.execStep( %> + alter table paid drop column site_id + <% ); + case 29: + up.execStep( %> + alter table registration drop column site_id + <% ); + case 30: + up.execStep( %> + alter table subscription drop column site_id + <% ); + case 31: + up.execStep( %> + alter table user_property drop column site_id + <% ); + case 32: + up.execStep( %> + alter table view_count drop column site_id + <% ); + case 33: + up.execStep( %> + alter table visited drop column site_id + <% ); + case 34: + up.execStep( %> + alter table file_site drop column site_id + <% ); + case 35: + up.execStep( %> + alter table file_site ADD PRIMARY KEY (name) + <% ); + case 36: + up.execStep( %> + alter table module drop column site_id + <% ); + case 37: + up.execStep( %> + alter table module ADD PRIMARY KEY (module_name) + <% ); + case 38: + up.execStep( %> + alter table site_property drop column site_id + <% ); + case 39: + up.execStep( %> + alter table site_property ADD PRIMARY KEY (key) + <% ); + case 40: + up.execStep( %> + alter table tweak drop column site_id + <% ); + case 41: + up.execStep( %> + alter table tweak ADD PRIMARY KEY (tweak_name) + <% ); + case 42: + up.execStep( %> + alter table tag drop column site_id + <% ); + case 43: + up.execStep( %> + CREATE UNIQUE INDEX tag_idx + ON tag (node_id, user_id, label); + <% ); + case 44: + up.execStep( %> + drop index tag_node_idx + <% ); + case 45: + up.execStep( %> + alter table user_ drop column site_id + <% ); + case 46: + up.execStep( %> + CREATE UNIQUE INDEX user_email_idx + ON user_ (lower(email)); + <% ); + case 47: + up.execStep( %> + CREATE UNIQUE INDEX user_name_idx + ON user_ (lower(name)); + <% ); + case 48: + up.execStep( %> + alter table node drop column site_id + <% ); + case 49: + up.execStep( %> + CREATE INDEX node_cookie_idx + ON node (cookie) + WHERE cookie IS NOT NULL; + <% ); + case 50: + up.execStep( %> + CREATE INDEX node_message_id_idx + ON node (lower(message_id)) + WHERE message_id IS NOT NULL; + <% ); + case 51: + up.execStep( %> + drop table embed_key + <% ); + case 52: + up.execStep( %> + delete from site_property where key = '__embedding_warning' + <% ); + case 53: + up.execStep( %> + alter table site drop column site_id + <% ); + case 54: + up.execStep( %> + alter table site add column one integer default 1 not null + <% ); + case 55: + up.execStep( %> + alter table site ADD PRIMARY KEY (one) + <% ); + case 56: + up.execStep( %> + alter table site add column content_type character varying; + <% ); + case 57: + up.execStep( %> + update configuration set naml=E'<override_macro name="site_style">\n\t<n.overridden/>\n\t#search-box { text-align:left; }\n</override_macro>' where name='searchBoxAlignment'; + <% ); + case 58: + up.execStep( %> + update site set ad_type=null where ad_type='ADBRITE' or ad_type='ADSENSE' + <% ); + + case 59: + up.execStep( %> + alter table node + add column unsafe character varying default 'new', + add column safe_version integer + <% ); + case 60: + up.execStep( %> + alter table user_ + add column unsafe character varying default 'new', + add column safe_version integer + <% ); + + case 61: + up.execStep( %> + update site set ad_type='HOPELESS' where ad_type='COPYRIGHTED' + <% ); + case 62: + up.execStep( %> + update site set ad_type='HOPELESS' where ad_type='OURS' + <% ); + case 63: + up.execStep( %> + update site set ad_type=null, ad_free=<%=Integer.MAX_VALUE%> where ad_type='NONE' + <% ); + + case 64: + up.execStep( %> + alter table node + add column is_safe boolean + <% ); + case 65: + up.execStep( %> + alter table user_ + add column is_safe boolean + <% ); + case 66: + up.execStep( null ); + case 67: + up.execStep( null ); + case 68: + up.execStep( %> + delete from tag where label='hide_ads' + <% ); + case 69: + up.execStep( %> + update site set ad_type=null where ad_type='SOME_ADSENSE' + <% ); + case 70: + up.execStep( %> + update site set ad_type='ADULT' where ad_type='HACK' + <% ); + case 71: + up.execStep( %> + update site set content_type=null where content_type='FAMILY' + <% ); + case 72: + up.execStep( %> + drop index if exists node_is_safe_idx + <% ); + case 73: + up.execStep( %> + drop index if exists user_is_safe_idx + <% ); + case 74: + up.execStep( %> + create index node_unsafe_idx on node(unsafe) where unsafe is not null and unsafe!='new' + <% ); + case 75: + up.execStep( %> + create index user_unsafe_idx on user_(unsafe) where unsafe is not null and unsafe!='new' + <% ); + case 76: + up.execStep( %> + update site set content_type=ad_type where content_type is null and ad_type is not null and ad_type!='HOPELESS' + <% ); + case 77: + up.execStep( %> + update site set ad_type=content_type where content_type is not null and ad_type is null + <% ); + case 78: + up.execStep( %> + update site set ad_type='ADULT' where content_type='ADULT' and ad_type='PORN' + <% ); + + case 79: + up.execStep( %> + alter table user_ + drop column unsafe, + drop column safe_version, + drop column is_safe + <% ); + case 80: + up.execStep( null ); + case 81: + up.execStep( null ); + case 82: + up.begin(); + up.exec( %> + alter table node + drop column unsafe, + drop column safe_version, + drop column is_safe + <% ); + up.exec( %> + alter table node + add column unsafe character varying, + add column safe_version integer, + add column is_safe boolean + <% ); + up.commit(); + case 83: + up.execStep( %> + create index node_unsafe_idx on node(unsafe) where safe_version is not null + <% ); + case 84: + up.execStep( %> + delete from module where module_name = 'topic_ads'; + <% ); + case 85: + up.execStep( %> + alter table node + drop column unsafe, + drop column safe_version, + drop column is_safe + <% ); + case 86: + up.execStep( %> + alter table site + add column when_created timestamp with time zone NOT NULL DEFAULT now() + <% ); + case 87: + up.execStep( %> + update site + set when_created = node.when_created + from node + where node.node_id = site.root_node_id + <% ); + + case 88: + up.execStep( %> + alter table site + add column is_safe boolean NOT NULL DEFAULT true + <% ); + case 89: + up.execStep( %> + update site set is_safe = false where ad_type is not null + <% ); + case 90: + up.execStep( %> + update site set when_created = now() where not is_safe + <% ); + case 91: + up.execStep( %> + alter table site rename column ad_type to old_ad_type + <% ); + case 92: + up.execStep( %> + alter table site rename column content_type to old_content_type + <% ); + case 93: + up.execStep( %> + alter table site + add column ad_credits_for_users boolean NOT NULL DEFAULT false + <% ); + case 94: + up.execStep( %> + alter table site + add column monthly_views integer NOT NULL DEFAULT 0 + <% ); + case 95: + up.execStep( %> + delete from tag where label='group:Administrators' and node_id is null and user_id = (select user_id from user_ where email = 'pedro@nabble.com') + <% ); + + case 96: + execCatching( %> + alter table tweak ADD PRIMARY KEY (tweak_name) + <%, "multiple primary keys for table" ); + case 97: + execCatching( %> + alter table mailing_list ADD PRIMARY KEY (node_id) + <%, "multiple primary keys for table" ); + case 98: +/* + execCatching( %> + alter table mailing_list ADD CONSTRAINT mailing_list_node_id_fkey FOREIGN KEY (node_id) REFERENCES node (node_id) ON DELETE CASCADE + <%, "already exists" ); +*/ + up.execStep( null ); + case 99: + execCatching( %> + CREATE INDEX forum_ml_idx ON mailing_list (lower(list_address)) + <%, "already exists" ); + case 100: + execCatching( %> + alter table node ADD PRIMARY KEY (node_id) + <%, "multiple primary keys for table" ); + case 101: + execCatching( %> + alter table configuration ADD PRIMARY KEY (name) + <%, "multiple primary keys for table" ); + case 102: + execCatching( %> + alter table file_avatar ADD PRIMARY KEY (user_id, name) + <%, "multiple primary keys for table" ); + case 103: + execCatching( %> + alter table file_node ADD PRIMARY KEY (node_id, name) + <%, "multiple primary keys for table" ); + case 104: + execCatching( %> + alter table file_site ADD PRIMARY KEY (name) + <%, "multiple primary keys for table" ); + case 105: + execCatching( %> + alter table file_temp ADD PRIMARY KEY (user_id, name) + <%, "multiple primary keys for table" ); + case 106: + execCatching( %> + CREATE INDEX file_temp_date_idx ON file_temp (date_) + <%, "already exists" ); + case 107: + execCatching( %> + alter table module ADD PRIMARY KEY (module_name) + <%, "multiple primary keys for table" ); + case 108: + execCatching( %> + CREATE INDEX node_cookie_idx ON node (cookie) WHERE cookie IS NOT NULL + <%, "already exists" ); + case 109: + execCatching( %> + CREATE INDEX node_export ON node (export_permalink) WHERE export_permalink IS NOT NULL + <%, "already exists" ); + case 110: + execCatching( %> + CREATE INDEX node_message_id_idx ON node (lower(message_id)) WHERE message_id IS NOT NULL + <%, "already exists" ); + case 111: + execCatching( %> + CREATE INDEX node_ml2_idx ON node (lower(parent_message_id)) WHERE lower(parent_message_id) IS NOT NULL + <%, "already exists" ); + case 112: + execCatching( %> + CREATE INDEX node_owner_idx ON node (owner_id, when_created) + <%, "already exists" ); + case 113: + execCatching( %> + CREATE UNIQUE INDEX node_parent2_idx ON node (parent_id, when_created, node_id) + <%, "already exists" ); + case 114: + execCatching( %> + CREATE UNIQUE INDEX node_parent_idx ON node (parent_id, last_node_date, node_id) + <%, "already exists" ); + case 115: + execCatching( %> + CREATE INDEX node_sent_idx ON node (when_sent) WHERE when_sent IS NOT NULL + <%, "already exists" ); + case 116: + execCatching( %> + CREATE UNIQUE INDEX pinned_node_idx ON node (parent_id, pin) WHERE pin IS NOT NULL + <%, "already exists" ); + case 117: + execCatching( %> + CREATE INDEX post_date ON node (when_created) + <%, "already exists" ); + case 118: + execCatching( %> + alter table node_msg ADD PRIMARY KEY (node_id) + <%, "multiple primary keys for table" ); + case 119: + execCatching( %> + alter table node_property ADD PRIMARY KEY (node_id, key) + <%, "multiple primary keys for table" ); + case 120: + execCatching( %> + alter table paid ADD PRIMARY KEY (user_id) + <%, "multiple primary keys for table" ); + case 121: + execCatching( %> + alter table registration ADD PRIMARY KEY (key_) + <%, "multiple primary keys for table" ); + case 122: + execCatching( %> + CREATE INDEX registration_date_idx ON registration (date_) + <%, "already exists" ); + case 123: + execCatching( %> + alter table site ADD PRIMARY KEY (one) + <%, "multiple primary keys for table" ); + case 124: + execCatching( %> + alter table site_property ADD PRIMARY KEY (key) + <%, "multiple primary keys for table" ); + case 125: + execCatching( %> + alter table subscription ADD PRIMARY KEY (user_id, node_id) + <%, "multiple primary keys for table" ); + case 126: + execCatching( %> + CREATE INDEX subscription_node_idx ON subscription (node_id) + <%, "already exists" ); + case 127: + execCatching( %> + CREATE UNIQUE INDEX tag_idx ON tag (node_id, user_id, label) + <%, "already exists" ); + case 128: + execCatching( %> + CREATE INDEX tag_user_idx ON tag (user_id) + <%, "already exists" ); + case 129: + execCatching( %> + alter table user_ ADD PRIMARY KEY (user_id) + <%, "multiple primary keys for table" ); + case 130: + execCatching( %> + CREATE UNIQUE INDEX user_email_idx ON user_ (lower(email)) + <%, "already exists" ); + case 131: + execCatching( %> + CREATE UNIQUE INDEX user_name_idx ON user_ (lower(name)) + <%, "already exists" ); + case 132: + execCatching( %> + alter table user_property ADD PRIMARY KEY (user_id, key) + <%, "multiple primary keys for table" ); + case 133: + execCatching( %> + alter table view_count ADD PRIMARY KEY (node_id) + <%, "multiple primary keys for table" ); + case 134: + execCatching( %> + alter table visited ADD PRIMARY KEY (user_id, node_id) + <%, "multiple primary keys for table" ); + case 135: + execCatching( %> + CREATE INDEX visited_last_node_idx ON visited (last_node_id) + <%, "already exists" ); + case 136: + execCatching( %> + CREATE INDEX visited_node_idx ON visited (node_id) + <%, "already exists" ); + + case 137: + execCatching( %> + ALTER TABLE ONLY file_avatar + ADD CONSTRAINT file_avatar_user_id_fkey FOREIGN KEY (user_id) REFERENCES user_(user_id) ON DELETE CASCADE + <%, "already exists" ); + case 138: + up.execStep( null ); + case 139: + execCatching( %> + ALTER TABLE ONLY file_temp + ADD CONSTRAINT file_temp_user_id_fkey FOREIGN KEY (user_id) REFERENCES user_(user_id) ON DELETE CASCADE + <%, "already exists" ); + case 140: + execCatching( %> + ALTER TABLE ONLY mailing_list + ADD CONSTRAINT mailing_list_node_id_fkey FOREIGN KEY (node_id) REFERENCES node(node_id) ON DELETE CASCADE + <%, "already exists" ); + case 141: + up.execStep( null ); + case 142: + execCatching( %> + ALTER TABLE ONLY node + ADD CONSTRAINT node_new_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES user_(user_id) + <%, "already exists" ); + case 143: + execCatching( %> + ALTER TABLE ONLY node + ADD CONSTRAINT node_node_id_fkey FOREIGN KEY (node_id) REFERENCES node_msg(node_id) DEFERRABLE INITIALLY DEFERRED + <%, "already exists" ); + case 144: + up.execStep( null ); + case 145: + up.execStep( null ); + case 146: + execCatching( %> + ALTER TABLE ONLY paid + ADD CONSTRAINT paid_user_id_fkey FOREIGN KEY (user_id) REFERENCES user_(user_id) ON DELETE CASCADE + <%, "already exists" ); + case 147: + execCatching( %> + ALTER TABLE ONLY site + ADD CONSTRAINT site_root_node_id_fkey FOREIGN KEY (root_node_id) REFERENCES node(node_id) DEFERRABLE INITIALLY DEFERRED + <%, "already exists" ); + case 148: + up.execStep( null ); + case 149: + execCatching( %> + ALTER TABLE ONLY subscription + ADD CONSTRAINT subscription_user_id_fkey FOREIGN KEY (user_id) REFERENCES user_(user_id) ON DELETE CASCADE + <%, "already exists" ); + case 150: + up.execStep( null ); + case 151: + execCatching( %> + ALTER TABLE ONLY tag + ADD CONSTRAINT tag_user_id_fkey FOREIGN KEY (user_id) REFERENCES user_(user_id) ON DELETE CASCADE + <%, "already exists" ); + case 152: + execCatching( %> + ALTER TABLE ONLY user_property + ADD CONSTRAINT user_property_user_id_fkey FOREIGN KEY (user_id) REFERENCES user_(user_id) ON DELETE CASCADE + <%, "already exists" ); + case 153: + up.execStep( null ); + case 154: + up.execStep( null ); + case 155: + up.execStep( null ); + case 156: + execCatching( %> + ALTER TABLE ONLY visited + ADD CONSTRAINT visited_user_id_fkey FOREIGN KEY (user_id) REFERENCES user_(user_id) ON DELETE CASCADE + <%, "already exists" ); + case 157: + up.execStep( null ); + + // problematic constraints + case 158: + execCatching( %> + ALTER TABLE ONLY file_node + ADD CONSTRAINT file_node_node_id_fkey FOREIGN KEY (node_id) REFERENCES node(node_id) ON DELETE CASCADE + <%, "already exists" ); + case 159: + execCatching( %> + ALTER TABLE ONLY node_msg + ADD CONSTRAINT node_msg_node_id_fkey FOREIGN KEY (node_id) REFERENCES node(node_id) ON DELETE CASCADE + <%, "already exists" ); + case 160: + execCatching( %> + ALTER TABLE ONLY node + ADD CONSTRAINT node_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES node(node_id) ON DELETE CASCADE + <%, "already exists" ); + case 161: + execCatching( %> + ALTER TABLE ONLY node_property + ADD CONSTRAINT node_property_node_id_fkey FOREIGN KEY (node_id) REFERENCES node(node_id) ON DELETE CASCADE + <%, "already exists" ); + case 162: + execCatching( %> + ALTER TABLE ONLY subscription + ADD CONSTRAINT subscription_node_id_fkey FOREIGN KEY (node_id) REFERENCES node(node_id) ON DELETE CASCADE + <%, "already exists" ); + case 163: + execCatching( %> + ALTER TABLE ONLY tag + ADD CONSTRAINT tag_node_id_fkey FOREIGN KEY (node_id) REFERENCES node(node_id) ON DELETE CASCADE + <%, "already exists" ); + case 164: + execCatching( %> + ALTER TABLE ONLY view_count + ADD CONSTRAINT view_count_node_id_fkey FOREIGN KEY (node_id) REFERENCES node(node_id) ON DELETE CASCADE + <%, "already exists" ); + case 165: + execCatching( %> + ALTER TABLE ONLY visited + ADD CONSTRAINT visited_last_node_id_fkey FOREIGN KEY (last_node_id) REFERENCES node(node_id) ON DELETE CASCADE + <%, "already exists" ); + case 166: + execCatching( %> + ALTER TABLE ONLY visited + ADD CONSTRAINT visited_node_id_fkey FOREIGN KEY (node_id) REFERENCES node(node_id) ON DELETE CASCADE + <%, "already exists" ); + } + } +} +%>